source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 63:9f8c9611848a

Last change on this file since 63:9f8c9611848a was 63:9f8c9611848a, checked in by casties, 11 years ago

fixed bug with new rectangle shapes. added limit, offset and sortBy parameters to annotator/ and annotator/search.

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