source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 58:f5c0e6df7e88

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

made uri prefixes in store configurable.

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