source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 64:c48435e7f312

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

fix npe when an annotation has no creator.

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