source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 19:f0f55ab768c9

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

more work on HTML UI.

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