source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 16:794077e6288c

Last change on this file since 16:794077e6288c was 16:794077e6288c, checked in by casties, 12 years ago

CLOSED - # 252: Tags for Annotations
https://it-dev.mpiwg-berlin.mpg.de/tracs/mpdl-project-software/ticket/252

File size: 21.5 KB
Line 
1/**
2 * Base class for Annotator resource classes.
3 */
4package de.mpiwg.itgroup.annotations.restlet;
5
6import java.io.UnsupportedEncodingException;
7import java.security.InvalidKeyException;
8import java.security.SignatureException;
9import java.text.SimpleDateFormat;
10import java.util.ArrayList;
11import java.util.Calendar;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Set;
15import java.util.regex.Matcher;
16import java.util.regex.Pattern;
17
18import javax.servlet.ServletContext;
19
20import net.oauth.jsontoken.Checker;
21import net.oauth.jsontoken.JsonToken;
22import net.oauth.jsontoken.JsonTokenParser;
23import net.oauth.jsontoken.SystemClock;
24import net.oauth.jsontoken.crypto.HmacSHA256Verifier;
25import net.oauth.jsontoken.crypto.Verifier;
26
27import org.apache.commons.codec.binary.Base64;
28import org.apache.log4j.Logger;
29import org.json.JSONArray;
30import org.json.JSONException;
31import org.json.JSONObject;
32import org.restlet.data.Form;
33import org.restlet.data.Status;
34import org.restlet.representation.Representation;
35import org.restlet.resource.Options;
36import org.restlet.resource.ServerResource;
37
38import de.mpiwg.itgroup.annotations.Actor;
39import de.mpiwg.itgroup.annotations.Annotation;
40import de.mpiwg.itgroup.annotations.Annotation.FragmentTypes;
41import de.mpiwg.itgroup.annotations.Group;
42import de.mpiwg.itgroup.annotations.Person;
43import de.mpiwg.itgroup.annotations.neo4j.AnnotationStore;
44import de.mpiwg.itgroup.annotations.old.NS;
45
46/**
47 * Base class for Annotator resource classes.
48 *
49 * @author dwinter, casties
50 *
51 */
52public abstract class AnnotatorResourceImpl extends ServerResource {
53
54    protected static Logger logger = Logger.getLogger(AnnotatorResourceImpl.class);
55
56    private AnnotationStore store;
57
58    protected String getAllowedMethodsForHeader() {
59        return "OPTIONS,GET,POST";
60    }
61
62    protected AnnotationStore getAnnotationStore() {
63        if (store == null) {
64            ServletContext sc = (ServletContext) getContext().getServerDispatcher().getContext().getAttributes()
65                    .get("org.restlet.ext.servlet.ServletContext");
66            logger.debug("Getting AnnotationStore from Context");
67            store = (AnnotationStore) sc.getAttribute(RestServer.ANNSTORE_KEY);
68        }
69        return store;
70    }
71
72    public String encodeJsonId(String id) {
73        try {
74            return Base64.encodeBase64URLSafeString(id.getBytes("UTF-8"));
75        } catch (UnsupportedEncodingException e) {
76            return null;
77        }
78    }
79
80    public String decodeJsonId(String id) {
81        try {
82            return new String(Base64.decodeBase64(id), "UTF-8");
83        } catch (UnsupportedEncodingException e) {
84            return null;
85        }
86    }
87
88    /**
89     * Handle options request to allow CORS for AJAX.
90     *
91     * @param entity
92     */
93    @Options
94    public void doOptions(Representation entity) {
95        logger.debug("AnnotatorResourceImpl doOptions!");
96        setCorsHeaders();
97    }
98
99    /**
100     * set headers to allow CORS for AJAX.
101     */
102    protected void setCorsHeaders() {
103        Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
104        if (responseHeaders == null) {
105            responseHeaders = new Form();
106            getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
107        }
108        responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
109        // echo back Origin and Request-Headers
110        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
111        String origin = requestHeaders.getFirstValue("Origin", true);
112        if (origin == null) {
113            responseHeaders.add("Access-Control-Allow-Origin", "*");
114        } else {
115            responseHeaders.add("Access-Control-Allow-Origin", origin);
116        }
117        String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
118        if (allowHeaders != null) {
119            responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
120        }
121        responseHeaders.add("Access-Control-Allow-Credentials", "true");
122        responseHeaders.add("Access-Control-Max-Age", "60");
123    }
124
125    /**
126     * returns if authentication information from headers is valid.
127     *
128     * @param entity
129     * @return
130     */
131    public boolean isAuthenticated(Representation entity) {
132        return (checkAuthToken(entity) != null);
133    }
134
135    /**
136     * checks Annotator Auth plugin authentication information from headers.
137     * returns userId if successful.
138     *
139     * @param entity
140     * @return
141     */
142    public String checkAuthToken(Representation entity) {
143        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
144        String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
145        if (authToken == null) return null;
146        // decode token first to get consumer key
147        JsonToken token = new JsonTokenParser(null, null).deserialize(authToken);
148        String userId = token.getParamAsPrimitive("userId").getAsString();
149        String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString();
150        // get stored consumer secret for key
151        RestServer restServer = (RestServer) getApplication();
152        String consumerSecret = restServer.getConsumerSecret(consumerKey);
153        logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
154        if (consumerSecret == null) {
155            return null;
156        }
157        // logger.debug(String.format("token=%s tokenString=%s signatureAlgorithm=%s",token,token.getTokenString(),token.getSignatureAlgorithm()));
158        try {
159            List<Verifier> verifiers = new ArrayList<Verifier>();
160            // we only do HS256 yet
161            verifiers.add(new HmacSHA256Verifier(consumerSecret.getBytes("UTF-8")));
162            // verify token signature(should really be static...)
163            new JsonTokenParser(new SystemClock(), null, (Checker[]) null).verify(token, verifiers);
164        } catch (SignatureException e) {
165            // TODO Auto-generated catch block
166            e.printStackTrace();
167        } catch (InvalidKeyException e) {
168            // TODO Auto-generated catch block
169            e.printStackTrace();
170        } catch (UnsupportedEncodingException e) {
171            // TODO Auto-generated catch block
172            e.printStackTrace();
173        }
174        // must be ok then
175        logger.debug("auth OK! user=" + userId);
176        return userId;
177    }
178
179    /**
180     * creates Annotator-JSON from an Annotation object.
181     *
182     * @param annot
183     * @param forAnonymous TODO
184     * @return
185     */
186    public JSONObject createAnnotatorJson(Annotation annot, boolean forAnonymous) {
187        // return user as a JSON object (otherwise just as string)
188        boolean makeUserObject = true;
189        JSONObject jo = new JSONObject();
190        try {
191            jo.put("text", annot.getBodyText());
192            jo.put("uri", annot.getTargetBaseUri());
193
194            /*
195             * user
196             */
197            if (makeUserObject) {
198                // create user object
199                JSONObject userObject = new JSONObject();
200                Actor creator = annot.getCreator();
201                // save creator as uri
202                userObject.put("uri", creator.getUri());
203                // make short user id
204                String userId = creator.getIdString();
205                // set as id
206                userObject.put("id", userId);
207                // get full name
208                String userName = creator.getName();
209                if (userName == null) {
210                    RestServer restServer = (RestServer) getApplication();
211                    userName = restServer.getFullNameFromLdap(userId);
212                }
213                userObject.put("name", userName);
214                // save user object
215                jo.put("user", userObject);
216            } else {
217                // save user as string
218                jo.put("user", annot.getCreatorUri());
219            }
220
221            /*
222             * ranges
223             */
224            if (annot.getTargetFragment() != null) {
225                // we only look at the first xpointer
226                List<String> fragments = new ArrayList<String>();
227                fragments.add(annot.getTargetFragment());
228                FragmentTypes xt = annot.getFragmentType();
229                if (xt == FragmentTypes.XPOINTER) {
230                    jo.put("ranges", transformToRanges(fragments));
231                } else if (xt == FragmentTypes.AREA) {
232                    jo.put("areas", transformToAreas(fragments));
233                }
234            }
235           
236            /*
237             * permissions
238             */
239            JSONObject perms = new JSONObject();
240            jo.put("permissions", perms);
241            // admin
242            JSONArray adminPerms = new JSONArray();
243            perms.put("admin", adminPerms);
244            Actor adminPerm = annot.getAdminPermission();
245            if (adminPerm != null) {
246                adminPerms.put(adminPerm.getIdString());
247            } else if (forAnonymous) {
248                // set something because its not allowed for anonymous
249                adminPerms.put("not-you");
250            }
251            // delete
252            JSONArray deletePerms = new JSONArray();
253            perms.put("delete", deletePerms);
254            Actor deletePerm = annot.getDeletePermission();
255            if (deletePerm != null) {
256                deletePerms.put(deletePerm.getIdString());
257            } else if (forAnonymous) {
258                // set something because its not allowed for anonymous
259                deletePerms.put("not-you");
260            }
261            // update
262            JSONArray updatePerms = new JSONArray();
263            perms.put("update", updatePerms);
264            Actor updatePerm = annot.getUpdatePermission();
265            if (updatePerm != null) {
266                updatePerms.put(updatePerm.getIdString());
267            } else if (forAnonymous) {
268                // set something because its not allowed for anonymous
269                updatePerms.put("not-you");
270            }
271            // read
272            JSONArray readPerms = new JSONArray();
273            perms.put("read", readPerms);
274            Actor readPerm = annot.getReadPermission();
275            if (readPerm != null) {
276                readPerms.put(readPerm.getIdString());
277            }
278           
279            /*
280             * tags
281             */
282            Set<String> tagset = annot.getTags(); 
283            if (tagset != null) {
284                JSONArray tags = new JSONArray();
285                jo.put("tags", tags);
286                for (String tag : tagset) {
287                    tags.put(tag);
288                }
289            }
290           
291            /*
292             * id
293             */
294            // encode Annotation URL (=id) in base64
295            String annotUrl = annot.getUri();
296            String annotId = encodeJsonId(annotUrl);
297            jo.put("id", annotId);
298            return jo;
299        } catch (JSONException e) {
300            // TODO Auto-generated catch block
301            e.printStackTrace();
302        }
303        return null;
304    }
305
306    private JSONArray transformToRanges(List<String> xpointers) {
307
308        JSONArray ja = new JSONArray();
309
310        Pattern rg = Pattern
311                .compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
312        Pattern rg1 = Pattern.compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");
313
314        try {
315            for (String xpointer : xpointers) {
316                // String decoded = URLDecoder.decode(xpointer, "utf-8");
317                String decoded = xpointer;
318                Matcher m = rg.matcher(decoded);
319
320                if (m.find()) {
321                    {
322                        JSONObject jo = new JSONObject();
323                        jo.put("start", m.group(1));
324                        jo.put("startOffset", m.group(2));
325                        jo.put("end", m.group(3));
326                        jo.put("endOffset", m.group(4));
327                        ja.put(jo);
328                    }
329                }
330                m = rg1.matcher(xpointer);
331                if (m.find()) {
332                    JSONObject jo = new JSONObject();
333                    jo.put("start", m.group(1));
334                    jo.put("startOffset", m.group(2));
335
336                    ja.put(jo);
337                }
338            }
339        } catch (JSONException e) {
340            // TODO Auto-generated catch block
341            e.printStackTrace();
342        }
343        return ja;
344    }
345
346    private JSONArray transformToAreas(List<String> xpointers) {
347
348        JSONArray ja = new JSONArray();
349
350        Pattern rg = Pattern.compile("xywh=(\\w*:)([\\d\\.]+),([\\d\\.]+),([\\d\\.]+),([\\d\\.]+)");
351
352        try {
353            for (String xpointer : xpointers) {
354                // String decoded = URLDecoder.decode(xpointer, "utf-8");
355                String decoded = xpointer;
356                Matcher m = rg.matcher(decoded);
357
358                if (m.find()) {
359                    {
360                        JSONObject jo = new JSONObject();
361                        @SuppressWarnings("unused")
362                        String unit = m.group(1);
363                        jo.put("x", m.group(2));
364                        jo.put("y", m.group(3));
365                        jo.put("width", m.group(4));
366                        jo.put("height", m.group(5));
367                        ja.put(jo);
368                    }
369                }
370            }
371        } catch (JSONException e) {
372            // TODO Auto-generated catch block
373            e.printStackTrace();
374        }
375        return ja;
376    }
377
378    protected String parseArea(JSONObject area) throws JSONException {
379        String x = area.getString("x");
380        String y = area.getString("y");
381        String width = "0";
382        String height = "0";
383        if (area.has("width")) {
384            width = area.getString("width");
385            height = area.getString("height");
386        }
387        String fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height);
388        return fragment;
389    }
390
391    protected String parseRange(JSONObject range) throws JSONException {
392        String start = range.getString("start");
393        String end = range.getString("end");
394        String startOffset = range.getString("startOffset");
395        String endOffset = range.getString("endOffset");
396
397        String fragment = String.format(
398                "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))", start,
399                startOffset, end, endOffset);
400        return fragment;
401    }
402
403    /**
404     * Creates an Annotation object with data from JSON.
405     *
406     * uses the specification from the annotator project: {@link https
407     * ://github.com/okfn/annotator/wiki/Annotation-format}
408     *
409     * The username will be transformed to an URI if not given already as URI,
410     * if not it will set to the MPIWG namespace defined in
411     * de.mpiwg.itgroup.annotationManager.Constants.NS
412     *
413     * @param jo
414     * @return
415     * @throws JSONException
416     * @throws UnsupportedEncodingException
417     */
418    public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException {
419        return updateAnnotation(new Annotation(), jo, entity);
420    }
421
422    /**
423     * Updates an Annotation object with data from JSON.
424     *
425     * uses the specification from the annotator project: {@link https
426     * ://github.com/okfn/annotator/wiki/Annotation-format}
427     *
428     * The username will be transformed to an URI if not given already as URI,
429     * if not it will set to the MPIWG namespace defined in
430     * de.mpiwg.itgroup.annotationManager.Constants.NS
431     *
432     * @param annot
433     * @param jo
434     * @return
435     * @throws JSONException
436     * @throws UnsupportedEncodingException
437     */
438    public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException,
439            UnsupportedEncodingException {
440        /*
441         * target uri
442         */
443        if (jo.has("uri")) {
444            annot.setTargetBaseUri(jo.getString("uri"));
445        }
446        /*
447         * annotation text
448         */
449        if (jo.has("text")) {
450            annot.setBodyText(jo.getString("text"));
451        }
452        /*
453         * check authentication
454         */
455        String authUser = checkAuthToken(entity);
456        if (authUser == null) {
457            /*
458             * // try http auth User httpUser = getHttpAuthUser(entity); if
459             * (httpUser == null) {
460             */
461            setStatus(Status.CLIENT_ERROR_FORBIDDEN);
462            return null;
463            /*
464             * } authUser = httpUser.getIdentifier();
465             */
466        }
467        /*
468         * get or create creator object
469         */
470        Actor creator = annot.getCreator();
471        if (creator == null) {
472            creator = new Person();
473            annot.setCreator(creator);
474        }
475        // username not required, if no username given authuser will be used
476        String username = null;
477        String userUri = creator.getUri();
478        if (jo.has("user")) {
479            if (jo.get("user") instanceof String) {
480                // user is just a String
481                username = jo.getString("user");
482                creator.setId(username);
483                // TODO: what if username and authUser are different?
484            } else {
485                // user is an object
486                JSONObject user = jo.getJSONObject("user");
487                if (user.has("id")) {
488                    String id = user.getString("id");
489                    creator.setId(id);
490                    username = id;
491                }
492                if (user.has("uri")) {
493                    userUri = user.getString("uri");
494                }
495            }
496        }
497        if (username == null) {
498            username = authUser;
499        }
500        // try to get full name
501        if (creator.getName() == null && username != null) {
502            RestServer restServer = (RestServer) getApplication();
503            String fullName = restServer.getFullNameFromLdap(username);
504            creator.setName(fullName);
505        }
506        // userUri should be a URI, if not it will set to the MPIWG namespace
507        if (userUri == null) {
508            if (username.startsWith("http")) {
509                userUri = username;
510            } else {
511                userUri = NS.MPIWG_PERSONS_URL + username;
512            }
513        }
514        // TODO: should we overwrite the creator?
515        if (creator.getUri() == null) {
516            creator.setUri(userUri);
517        }
518        /*
519         * creation date
520         */
521        if (annot.getCreated() == null) {
522            // set creation date
523            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
524            String ct = format.format(Calendar.getInstance().getTime());
525            annot.setCreated(ct);
526        }
527
528        /*
529         * create xpointer from the first range/area
530         */
531        if (jo.has("ranges")) {
532            JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
533            annot.setFragmentType(FragmentTypes.XPOINTER);
534            String fragment = parseRange(ranges);
535            annot.setTargetFragment(fragment);
536        }
537        if (jo.has("areas")) {
538            JSONObject area = jo.getJSONArray("areas").getJSONObject(0);
539            annot.setFragmentType(FragmentTypes.AREA);
540            String fragment = parseArea(area);
541            annot.setTargetFragment(fragment);
542        }
543
544        /*
545         * permissions
546         */
547        if (jo.has("permissions")) {
548            JSONObject permissions = jo.getJSONObject("permissions");
549            if (permissions.has("admin")) {
550                JSONArray perms = permissions.getJSONArray("admin");
551                Actor actor = getActorFromPermissions(perms);
552                annot.setAdminPermission(actor);
553            }
554            if (permissions.has("delete")) {
555                JSONArray perms = permissions.getJSONArray("delete");
556                Actor actor = getActorFromPermissions(perms);
557                annot.setDeletePermission(actor);
558            }
559            if (permissions.has("update")) {
560                JSONArray perms = permissions.getJSONArray("update");
561                Actor actor = getActorFromPermissions(perms);
562                annot.setUpdatePermission(actor);
563            }
564            if (permissions.has("read")) {
565                JSONArray perms = permissions.getJSONArray("read");
566                Actor actor = getActorFromPermissions(perms);
567                annot.setReadPermission(actor);
568            }
569        }
570
571        /*
572         * tags
573         */
574        if (jo.has("tags")) {
575            HashSet<String> tagset = new HashSet<String>();
576            JSONArray tags = jo.getJSONArray("tags");
577            for (int i = 0; i < tags.length(); ++i) {
578                tagset.add(tags.getString(i));
579            }
580            annot.setTags(tagset);
581        }
582
583       
584        return annot;
585    }
586
587    @SuppressWarnings("unused") // i in for loop
588    protected Actor getActorFromPermissions(JSONArray perms) throws JSONException {
589        Actor actor = null;
590        for (int i = 0; i < perms.length(); ++i) {
591            String perm = perms.getString(i);
592            if (perm.toLowerCase().startsWith("group:")) {
593                String groupId = perm.substring(6);
594                actor = new Group(groupId);
595            } else {
596                actor = new Person(perm);
597            }
598            // we just take the first one
599            break;
600        }
601        return actor;
602    }
603
604}
Note: See TracBrowser for help on using the repository browser.