source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 52:a52c597075dc

Last change on this file since 52:a52c597075dc was 52:a52c597075dc, checked in by casties, 12 years ago

more resilience to ranges and areas in JSON

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