source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 57:4efb21cf0ce0

Last change on this file since 57:4efb21cf0ce0 was 57:4efb21cf0ce0, checked in by casties, 11 years ago

new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.

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