source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 61:b8ef15c8c4a5

Last change on this file since 61:b8ef15c8c4a5 was 61:b8ef15c8c4a5, checked in by casties, 11 years ago

implemented new shape format for image annotations.
minor cleanups.

File size: 23.7 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("point")) {
390            String x = geom.getString("x");
391            String y = geom.getString("y");
392            String width = shape.getString("width");
393            String height = shape.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
420        String fragment = String.format(
421                "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))", start,
422                startOffset, end, endOffset);
423        return fragment;
424    }
425
426    /**
427     * Creates an Annotation object with data from JSON.
428     *
429     * uses the specification from the annotator project: {@link https
430     * ://github.com/okfn/annotator/wiki/Annotation-format}
431     *
432     * The username will be transformed to an URI if not given already as URI,
433     * if not it will set to the MPIWG namespace defined in
434     * de.mpiwg.itgroup.annotationManager.Constants.NS
435     *
436     * @param jo
437     * @return
438     * @throws JSONException
439     * @throws UnsupportedEncodingException
440     */
441    public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException {
442        return updateAnnotation(new Annotation(), jo, entity);
443    }
444
445    /**
446     * Updates an Annotation object with data from JSON.
447     *
448     * uses the specification from the annotator project: {@link https
449     * ://github.com/okfn/annotator/wiki/Annotation-format}
450     *
451     * The username will be transformed to an URI if not given already as URI,
452     * if not it will set to the MPIWG namespace defined in
453     * de.mpiwg.itgroup.annotationManager.Constants.NS
454     *
455     * @param annot
456     * @param jo
457     * @return
458     * @throws JSONException
459     * @throws UnsupportedEncodingException
460     */
461    public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException,
462            UnsupportedEncodingException {
463        /*
464         * target uri
465         */
466        if (jo.has("uri")) {
467            annot.setTarget(new Target(jo.getString("uri")));
468        }
469        /*
470         * resource uri
471         */
472        if (jo.has("resource")) {
473            annot.setResource(new Resource(jo.getString("resource")));
474        }
475        /*
476         * annotation text
477         */
478        if (jo.has("text")) {
479            annot.setBodyText(jo.getString("text"));
480        }
481        /*
482         * check authentication
483         */
484        String authUser = checkAuthToken(entity);
485        if (authUser == null) {
486            /*
487             * // try http auth User httpUser = getHttpAuthUser(entity); if
488             * (httpUser == null) {
489             */
490            setStatus(Status.CLIENT_ERROR_FORBIDDEN);
491            return null;
492            /*
493             * } authUser = httpUser.getIdentifier();
494             */
495        }
496        /*
497         * get or create creator object
498         */
499        Actor creator = annot.getCreator();
500        if (creator == null) {
501            creator = new Person();
502            annot.setCreator(creator);
503        }
504        // username not required, if no username given authuser will be used
505        String username = null;
506        String userUri = creator.getUri();
507        if (jo.has("user")) {
508            if (jo.get("user") instanceof String) {
509                // user is just a String
510                username = jo.getString("user");
511                creator.setId(username);
512                // TODO: what if username and authUser are different?
513            } else {
514                // user is an object
515                JSONObject user = jo.getJSONObject("user");
516                if (user.has("id")) {
517                    String id = user.getString("id");
518                    creator.setId(id);
519                    username = id;
520                }
521                if (user.has("uri")) {
522                    userUri = user.getString("uri");
523                }
524            }
525        }
526        if (username == null) {
527            username = authUser;
528        }
529        // try to get full name
530        if (creator.getName() == null && username != null) {
531            BaseRestlet restServer = (BaseRestlet) getApplication();
532            String fullName = restServer.getFullNameFromLdap(username);
533            creator.setName(fullName);
534        }
535        // userUri should be a URI, if not it will set to the MPIWG namespace
536        if (userUri == null) {
537            if (username.startsWith("http")) {
538                userUri = username;
539            } else {
540                userUri = BaseRestlet.PERSONS_URI_PREFIX + username;
541            }
542        }
543        // TODO: should we overwrite the creator?
544        if (creator.getUri() == null) {
545            creator.setUri(userUri);
546        }
547        /*
548         * creation date
549         */
550        if (annot.getCreated() == null) {
551            // set creation date
552            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
553            String ct = format.format(Calendar.getInstance().getTime());
554            annot.setCreated(ct);
555        }
556
557        /*
558         * create fragment from the first range/area
559         */
560        try {
561            if (jo.has("ranges")) {
562                JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
563                annot.setFragmentType(FragmentTypes.XPOINTER);
564                String fragment = parseRange(ranges);
565                annot.setTargetFragment(fragment);
566            }
567        } catch (JSONException e) {
568            // nothing to do
569        }
570        try {
571            if (jo.has("shapes")) {
572                JSONObject shapes = jo.getJSONArray("shapes").getJSONObject(0);
573                annot.setFragmentType(FragmentTypes.AREA);
574                String fragment = parseShape(shapes);
575                annot.setTargetFragment(fragment);
576            }
577        } catch (JSONException e) {
578            // nothing to do
579        }
580        // deprecated areas type
581        try {
582            if (jo.has("areas")) {
583                JSONObject area = jo.getJSONArray("areas").getJSONObject(0);
584                annot.setFragmentType(FragmentTypes.AREA);
585                String fragment = parseArea(area);
586                annot.setTargetFragment(fragment);
587            }
588        } catch (JSONException e) {
589            // nothing to do
590        }
591
592        /*
593         * permissions
594         */
595        if (jo.has("permissions")) {
596            JSONObject permissions = jo.getJSONObject("permissions");
597            if (permissions.has("admin")) {
598                JSONArray perms = permissions.getJSONArray("admin");
599                Actor actor = getActorFromPermissions(perms);
600                annot.setAdminPermission(actor);
601            }
602            if (permissions.has("delete")) {
603                JSONArray perms = permissions.getJSONArray("delete");
604                Actor actor = getActorFromPermissions(perms);
605                annot.setDeletePermission(actor);
606            }
607            if (permissions.has("update")) {
608                JSONArray perms = permissions.getJSONArray("update");
609                Actor actor = getActorFromPermissions(perms);
610                annot.setUpdatePermission(actor);
611            }
612            if (permissions.has("read")) {
613                JSONArray perms = permissions.getJSONArray("read");
614                Actor actor = getActorFromPermissions(perms);
615                annot.setReadPermission(actor);
616            }
617        }
618
619        /*
620         * tags
621         */
622        if (jo.has("tags")) {
623            HashSet<String> tagset = new HashSet<String>();
624            JSONArray tags = jo.getJSONArray("tags");
625            for (int i = 0; i < tags.length(); ++i) {
626                tagset.add(tags.getString(i));
627            }
628            annot.setTags(tagset);
629        }
630
631        return annot;
632    }
633
634    @SuppressWarnings("unused")
635    // i in for loop
636    protected Actor getActorFromPermissions(JSONArray perms) throws JSONException {
637        Actor actor = null;
638        for (int i = 0; i < perms.length(); ++i) {
639            String perm = perms.getString(i);
640            if (perm.toLowerCase().startsWith("group:")) {
641                String groupId = perm.substring(6);
642                actor = new Group(groupId);
643            } else {
644                actor = new Person(perm);
645            }
646            // we just take the first one
647            break;
648        }
649        return actor;
650    }
651
652    public float getFloat(String s) {
653        try {
654            return Float.parseFloat(s);
655        } catch (NumberFormatException e) {
656        }
657        return 0f;
658    }
659}
Note: See TracBrowser for help on using the repository browser.