source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 70:2b1e6df5e21a

Last change on this file since 70:2b1e6df5e21a was 70:2b1e6df5e21a, checked in by casties, 10 years ago

added lgpl_v3 license information.

File size: 25.4 KB
Line 
1/**
2 * Base class for Annotator resource classes.
3 */
4package de.mpiwg.itgroup.annotations.restlet;
5
6/*
7 * #%L
8 * AnnotationManager
9 * %%
10 * Copyright (C) 2012 - 2014 MPIWG Berlin
11 * %%
12 * This program is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU Lesser General Public License as
14 * published by the Free Software Foundation, either version 3 of the
15 * License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 * GNU General Lesser Public License for more details.
21 *
22 * You should have received a copy of the GNU General Lesser Public
23 * License along with this program.  If not, see
24 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
25 * #L%
26 */
27
28import java.io.UnsupportedEncodingException;
29import java.security.InvalidKeyException;
30import java.security.SignatureException;
31import java.text.SimpleDateFormat;
32import java.util.ArrayList;
33import java.util.Calendar;
34import java.util.HashSet;
35import java.util.List;
36import java.util.Set;
37import java.util.regex.Matcher;
38import java.util.regex.Pattern;
39
40import net.oauth.jsontoken.Checker;
41import net.oauth.jsontoken.JsonToken;
42import net.oauth.jsontoken.JsonTokenParser;
43import net.oauth.jsontoken.SystemClock;
44import net.oauth.jsontoken.crypto.HmacSHA256Verifier;
45import net.oauth.jsontoken.crypto.Verifier;
46
47import org.apache.commons.codec.binary.Base64;
48import org.apache.log4j.Logger;
49import org.json.JSONArray;
50import org.json.JSONException;
51import org.json.JSONObject;
52import org.restlet.data.Form;
53import org.restlet.data.Status;
54import org.restlet.representation.Representation;
55import org.restlet.resource.Options;
56import org.restlet.resource.ServerResource;
57
58import de.mpiwg.itgroup.annotations.Actor;
59import de.mpiwg.itgroup.annotations.Annotation;
60import de.mpiwg.itgroup.annotations.Annotation.FragmentTypes;
61import de.mpiwg.itgroup.annotations.Group;
62import de.mpiwg.itgroup.annotations.Person;
63import de.mpiwg.itgroup.annotations.Resource;
64import de.mpiwg.itgroup.annotations.Target;
65import de.mpiwg.itgroup.annotations.neo4j.AnnotationStore;
66
67/**
68 * Base class for Annotator resource classes.
69 *
70 * @author dwinter, casties
71 *
72 */
73public abstract class AnnotatorResourceImpl extends ServerResource {
74
75    protected static Logger logger = Logger.getLogger(AnnotatorResourceImpl.class);
76
77    private AnnotationStore store;
78
79    protected String getAllowedMethodsForHeader() {
80        return "OPTIONS,GET,POST";
81    }
82
83    protected AnnotationStore getAnnotationStore() {
84        if (store == null) {
85            store = ((BaseRestlet) getApplication()).getAnnotationStore();
86        }
87        return store;
88    }
89
90    public String encodeJsonId(String id) {
91        if (id == null)
92            return null;
93        try {
94            return Base64.encodeBase64URLSafeString(id.getBytes("UTF-8"));
95        } catch (UnsupportedEncodingException e) {
96            return null;
97        }
98    }
99
100    public String decodeJsonId(String id) {
101        if (id == null)
102            return null;
103        try {
104            return new String(Base64.decodeBase64(id), "UTF-8");
105        } catch (UnsupportedEncodingException e) {
106            return null;
107        }
108    }
109
110    /**
111     * Handle options request to allow CORS for AJAX.
112     *
113     * @param entity
114     */
115    @Options
116    public void doOptions(Representation entity) {
117        logger.debug("AnnotatorResourceImpl doOptions!");
118        setCorsHeaders();
119    }
120
121    /**
122     * set headers to allow CORS for AJAX.
123     */
124    protected void setCorsHeaders() {
125        Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
126        if (responseHeaders == null) {
127            responseHeaders = new Form();
128            getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
129        }
130        responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
131        // echo back Origin and Request-Headers
132        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
133        String origin = requestHeaders.getFirstValue("Origin", true);
134        if (origin == null) {
135            responseHeaders.add("Access-Control-Allow-Origin", "*");
136        } else {
137            responseHeaders.add("Access-Control-Allow-Origin", origin);
138        }
139        String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
140        if (allowHeaders != null) {
141            responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
142        }
143        responseHeaders.add("Access-Control-Allow-Credentials", "true");
144        responseHeaders.add("Access-Control-Max-Age", "60");
145    }
146
147    /**
148     * returns if authentication information from headers is valid.
149     *
150     * @param entity
151     * @return
152     */
153    public boolean isAuthenticated(Representation entity) {
154        return (checkAuthToken(entity) != null);
155    }
156
157    /**
158     * Checks Annotator Auth plugin authentication information from headers.
159     * Returns userId if successful. Returns "anonymous" in non-authorization
160     * mode.
161     *
162     * @param entity
163     * @return
164     */
165    public String checkAuthToken(Representation entity) {
166        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
167        String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
168        if (authToken == null) {
169            if (!((BaseRestlet) getApplication()).isAuthorizationMode()) {
170                return "anonymous";
171            }
172            return null;
173        }
174        // decode token first to get consumer key
175        JsonToken token = new JsonTokenParser(null, null).deserialize(authToken);
176        String userId = token.getParamAsPrimitive("userId").getAsString();
177        String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString();
178        // get stored consumer secret for key
179        BaseRestlet restServer = (BaseRestlet) getApplication();
180        String consumerSecret = restServer.getConsumerSecret(consumerKey);
181        logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
182        if (consumerSecret == null) {
183            return null;
184        }
185        // logger.debug(String.format("token=%s tokenString=%s signatureAlgorithm=%s",token,token.getTokenString(),token.getSignatureAlgorithm()));
186        try {
187            List<Verifier> verifiers = new ArrayList<Verifier>();
188            // we only do HS256 yet
189            verifiers.add(new HmacSHA256Verifier(consumerSecret.getBytes("UTF-8")));
190            // verify token signature(should really be static...)
191            new JsonTokenParser(new SystemClock(), null, (Checker[]) null).verify(token, verifiers);
192        } catch (SignatureException e) {
193            // TODO Auto-generated catch block
194            e.printStackTrace();
195        } catch (InvalidKeyException e) {
196            // TODO Auto-generated catch block
197            e.printStackTrace();
198        } catch (UnsupportedEncodingException e) {
199            // TODO Auto-generated catch block
200            e.printStackTrace();
201        }
202        // must be ok then
203        logger.debug("auth OK! user=" + userId);
204        return userId;
205    }
206
207    /**
208     * creates Annotator-JSON from an Annotation object.
209     *
210     * @param annot
211     * @param forAnonymous
212     *            TODO
213     * @return
214     */
215    public JSONObject createAnnotatorJson(Annotation annot, boolean forAnonymous) {
216        // return user as a JSON object (otherwise just as string)
217        boolean makeUserObject = true;
218        JSONObject jo = new JSONObject();
219        try {
220            jo.put("text", annot.getBodyText());
221            jo.put("uri", annot.getTargetBaseUri());
222            if (annot.getResourceUri() != null) {
223                jo.put("resource", annot.getResourceUri());
224            }
225
226            /*
227             * user
228             */
229            Actor creator = annot.getCreator();
230            if (creator != null) {
231                if (makeUserObject) {
232                    // create user object
233                    JSONObject userObject = new JSONObject();
234                    // save creator as uri
235                    userObject.put("uri", creator.getUri());
236                    // make short user id
237                    String userId = creator.getIdString();
238                    // set as id
239                    userObject.put("id", userId);
240                    // get full name
241                    String userName = creator.getName();
242                    if (userName == null) {
243                        BaseRestlet restServer = (BaseRestlet) getApplication();
244                        userName = restServer.getFullNameFromLdap(userId);
245                    }
246                    userObject.put("name", userName);
247                    // save user object
248                    jo.put("user", userObject);
249                } else {
250                    // save user as string
251                    jo.put("user", annot.getCreatorUri());
252                }
253            }
254
255            /*
256             * ranges
257             */
258            if (annot.getTargetFragment() != null) {
259                // we only look at the first xpointer
260                List<String> fragments = new ArrayList<String>();
261                fragments.add(annot.getTargetFragment());
262                FragmentTypes xt = annot.getFragmentType();
263                if (xt == FragmentTypes.XPOINTER) {
264                    jo.put("ranges", transformToRanges(fragments));
265                } else if (xt == FragmentTypes.AREA) {
266                    jo.put("shapes", transformToShapes(fragments));
267                }
268            }
269
270            /*
271             * permissions
272             */
273            JSONObject perms = new JSONObject();
274            jo.put("permissions", perms);
275            // admin
276            JSONArray adminPerms = new JSONArray();
277            perms.put("admin", adminPerms);
278            Actor adminPerm = annot.getAdminPermission();
279            if (adminPerm != null) {
280                adminPerms.put(adminPerm.getIdString());
281            } else if (forAnonymous) {
282                // set something because its not allowed for anonymous
283                adminPerms.put("not-you");
284            }
285            // delete
286            JSONArray deletePerms = new JSONArray();
287            perms.put("delete", deletePerms);
288            Actor deletePerm = annot.getDeletePermission();
289            if (deletePerm != null) {
290                deletePerms.put(deletePerm.getIdString());
291            } else if (forAnonymous) {
292                // set something because its not allowed for anonymous
293                deletePerms.put("not-you");
294            }
295            // update
296            JSONArray updatePerms = new JSONArray();
297            perms.put("update", updatePerms);
298            Actor updatePerm = annot.getUpdatePermission();
299            if (updatePerm != null) {
300                updatePerms.put(updatePerm.getIdString());
301            } else if (forAnonymous) {
302                // set something because its not allowed for anonymous
303                updatePerms.put("not-you");
304            }
305            // read
306            JSONArray readPerms = new JSONArray();
307            perms.put("read", readPerms);
308            Actor readPerm = annot.getReadPermission();
309            if (readPerm != null) {
310                readPerms.put(readPerm.getIdString());
311            }
312
313            /*
314             * tags
315             */
316            Set<String> tagset = annot.getTags();
317            if (tagset != null) {
318                JSONArray tags = new JSONArray();
319                jo.put("tags", tags);
320                for (String tag : tagset) {
321                    tags.put(tag);
322                }
323            }
324
325            /*
326             * id
327             */
328            // encode Annotation URL (=id) in base64
329            String annotUrl = annot.getUri();
330            String annotId = encodeJsonId(annotUrl);
331            jo.put("id", annotId);
332            return jo;
333        } catch (JSONException e) {
334            logger.error("Unable to create AnnotatorJSON!", e);
335        }
336        return null;
337    }
338
339    private JSONArray transformToRanges(List<String> xpointers) {
340        JSONArray ja = new JSONArray();
341        Pattern rg = Pattern
342                .compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
343        Pattern rg1 = Pattern.compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");
344        try {
345            for (String xpointer : xpointers) {
346                // String decoded = URLDecoder.decode(xpointer, "utf-8");
347                String decoded = xpointer;
348                Matcher m = rg.matcher(decoded);
349                if (m.find()) {
350                    JSONObject jo = new JSONObject();
351                    jo.put("start", m.group(1));
352                    jo.put("startOffset", m.group(2));
353                    jo.put("end", m.group(3));
354                    jo.put("endOffset", m.group(4));
355                    ja.put(jo);
356                }
357                m = rg1.matcher(xpointer);
358                if (m.find()) {
359                    JSONObject jo = new JSONObject();
360                    jo.put("start", m.group(1));
361                    jo.put("startOffset", m.group(2));
362                    ja.put(jo);
363                }
364            }
365        } catch (JSONException e) {
366            logger.error("Unable to transform to ranges!", e);
367        }
368        return ja;
369    }
370
371    private JSONArray transformToShapes(List<String> xpointers) {
372        JSONArray ja = new JSONArray();
373        Pattern rg = Pattern.compile("xywh=(\\w*):([\\d\\.]+),([\\d\\.]+),([\\d\\.]+),([\\d\\.]+)");
374        try {
375            for (String xpointer : xpointers) {
376                String decoded = xpointer;
377                Matcher m = rg.matcher(decoded);
378                if (m.find()) {
379                    String units = m.group(1);
380                    float x = getFloat(m.group(2));
381                    float y = getFloat(m.group(3));
382                    float width = getFloat(m.group(4));
383                    float height = getFloat(m.group(5));
384                    JSONObject shape = new JSONObject();
385                    JSONObject geom = new JSONObject();
386                    geom.put("units", units);
387                    geom.put("x", x);
388                    geom.put("y", y);
389                    if (width == 0 || height == 0) {
390                        shape.put("type", "point");
391                        shape.put("geometry", geom);
392                    } else {
393                        shape.put("type", "rectangle");
394                        geom.put("width", width);
395                        geom.put("height", height);
396                        shape.put("geometry", geom);
397                    }
398                    ja.put(shape);
399                }
400            }
401        } catch (JSONException e) {
402            logger.error("Unable to transform to shapes!", e);
403        }
404        return ja;
405    }
406
407    protected String parseShape(JSONObject shape) throws JSONException {
408        String fragment = null;
409        String type = shape.getString("type");
410        JSONObject geom = shape.getJSONObject("geometry");
411        if (type.equalsIgnoreCase("point")) {
412            String x = geom.getString("x");
413            String y = geom.getString("y");
414            fragment = String.format("xywh=fraction:%s,%s,0,0", x, y);
415        } else if (type.equalsIgnoreCase("rectangle")) {
416            String x = geom.getString("x");
417            String y = geom.getString("y");
418            String width = geom.getString("width");
419            String height = geom.getString("height");
420            fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height);
421        } else {
422            logger.error("Unable to parse this shape: " + shape);
423        }
424        return fragment;
425    }
426
427    protected String parseArea(JSONObject area) throws JSONException {
428        String x = area.getString("x");
429        String y = area.getString("y");
430        String width = "0";
431        String height = "0";
432        if (area.has("width")) {
433            width = area.getString("width");
434            height = area.getString("height");
435        }
436        String fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height);
437        return fragment;
438    }
439
440    protected String parseRange(JSONObject range) throws JSONException {
441        String start = range.getString("start");
442        String end = range.getString("end");
443        String startOffset = range.getString("startOffset");
444        String endOffset = range.getString("endOffset");
445        String fragment = String.format(
446                "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))", start,
447                startOffset, end, endOffset);
448        return fragment;
449    }
450
451    /**
452     * Creates an Annotation object with data from JSON.
453     *
454     * uses the specification from the annotator project: {@link https
455     * ://github.com/okfn/annotator/wiki/Annotation-format}
456     *
457     * The username will be transformed to an URI if not given already as URI,
458     * if not it will set to the MPIWG namespace defined in
459     * de.mpiwg.itgroup.annotationManager.Constants.NS
460     *
461     * @param jo
462     * @return
463     * @throws JSONException
464     * @throws UnsupportedEncodingException
465     */
466    public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException {
467        return updateAnnotation(new Annotation(), jo, entity);
468    }
469
470    /**
471     * Updates an Annotation object with data from JSON.
472     *
473     * uses the specification from the annotator project: {@link https
474     * ://github.com/okfn/annotator/wiki/Annotation-format}
475     *
476     * The username will be transformed to an URI if not given already as URI,
477     * if not it will set to the MPIWG namespace defined in
478     * de.mpiwg.itgroup.annotationManager.Constants.NS
479     *
480     * @param annot
481     * @param jo
482     * @return
483     * @throws JSONException
484     * @throws UnsupportedEncodingException
485     */
486    public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException,
487            UnsupportedEncodingException {
488        /*
489         * target uri
490         */
491        if (jo.has("uri")) {
492            annot.setTarget(new Target(jo.getString("uri")));
493        }
494        /*
495         * resource uri
496         */
497        if (jo.has("resource")) {
498            annot.setResource(new Resource(jo.getString("resource")));
499        }
500        /*
501         * annotation text
502         */
503        if (jo.has("text")) {
504            annot.setBodyText(jo.getString("text"));
505        }
506        /*
507         * check authentication
508         */
509        String authUser = checkAuthToken(entity);
510        if (authUser == null) {
511            /*
512             * // try http auth User httpUser = getHttpAuthUser(entity); if
513             * (httpUser == null) {
514             */
515            setStatus(Status.CLIENT_ERROR_FORBIDDEN);
516            return null;
517            /*
518             * } authUser = httpUser.getIdentifier();
519             */
520        }
521        /*
522         * get or create creator object
523         */
524        Actor creator = annot.getCreator();
525        if (creator == null) {
526            creator = new Person();
527            annot.setCreator(creator);
528        }
529        // username not required, if no username given authuser will be used
530        String username = null;
531        String userUri = creator.getUri();
532        if (jo.has("user")) {
533            if (jo.get("user") instanceof String) {
534                // user is just a String
535                username = jo.getString("user");
536                creator.setId(username);
537                // TODO: what if username and authUser are different?
538            } else {
539                // user is an object
540                JSONObject user = jo.getJSONObject("user");
541                if (user.has("id")) {
542                    String id = user.getString("id");
543                    creator.setId(id);
544                    username = id;
545                }
546                if (user.has("uri")) {
547                    userUri = user.getString("uri");
548                }
549            }
550        }
551        if (username == null) {
552            username = authUser;
553        }
554        // try to get full name
555        if (creator.getName() == null && username != null) {
556            BaseRestlet restServer = (BaseRestlet) getApplication();
557            String fullName = restServer.getFullNameFromLdap(username);
558            creator.setName(fullName);
559        }
560        // userUri should be a URI, if not it will set to the MPIWG namespace
561        if (userUri == null) {
562            if (username.startsWith("http")) {
563                userUri = username;
564            } else {
565                userUri = BaseRestlet.PERSONS_URI_PREFIX + username;
566            }
567        }
568        // TODO: should we overwrite the creator?
569        if (creator.getUri() == null) {
570            creator.setUri(userUri);
571        }
572        /*
573         * creation date
574         */
575        if (annot.getCreated() == null) {
576            // set creation date
577            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
578            String ct = format.format(Calendar.getInstance().getTime());
579            annot.setCreated(ct);
580        }
581
582        /*
583         * create fragment from the first range/area
584         */
585        try {
586            if (jo.has("ranges")) {
587                JSONArray ranges = jo.getJSONArray("ranges");
588                if (ranges.length() > 0) {
589                    JSONObject range = ranges.getJSONObject(0);
590                    annot.setFragmentType(FragmentTypes.XPOINTER);
591                    String fragment = parseRange(range);
592                    annot.setTargetFragment(fragment);
593                }
594            }
595        } catch (JSONException e) {
596            // nothing to do
597        }
598        try {
599            if (jo.has("shapes")) {
600                JSONArray shapes = jo.getJSONArray("shapes");
601                if (shapes.length() > 0) {
602                    JSONObject shape = shapes.getJSONObject(0);
603                    annot.setFragmentType(FragmentTypes.AREA);
604                    String fragment = parseShape(shape);
605                    annot.setTargetFragment(fragment);
606                }
607            }
608        } catch (JSONException e) {
609            // nothing to do
610        }
611        // deprecated areas type
612        try {
613            if (jo.has("areas")) {
614                JSONArray areas = jo.getJSONArray("areas");
615                if (areas.length() > 0) {
616                    JSONObject area = areas.getJSONObject(0);
617                    annot.setFragmentType(FragmentTypes.AREA);
618                    String fragment = parseArea(area);
619                    annot.setTargetFragment(fragment);
620                }
621            }
622        } catch (JSONException e) {
623            // nothing to do
624        }
625        // no fragment is an error
626        if (annot.getFragmentType() == null || annot.getTargetFragment() == null) {
627            throw new JSONException("Annotation has no valid target fragment!");
628        }
629
630        /*
631         * permissions
632         */
633        if (jo.has("permissions")) {
634            JSONObject permissions = jo.getJSONObject("permissions");
635            if (permissions.has("admin")) {
636                JSONArray perms = permissions.getJSONArray("admin");
637                Actor actor = getActorFromPermissions(perms);
638                annot.setAdminPermission(actor);
639            }
640            if (permissions.has("delete")) {
641                JSONArray perms = permissions.getJSONArray("delete");
642                Actor actor = getActorFromPermissions(perms);
643                annot.setDeletePermission(actor);
644            }
645            if (permissions.has("update")) {
646                JSONArray perms = permissions.getJSONArray("update");
647                Actor actor = getActorFromPermissions(perms);
648                annot.setUpdatePermission(actor);
649            }
650            if (permissions.has("read")) {
651                JSONArray perms = permissions.getJSONArray("read");
652                Actor actor = getActorFromPermissions(perms);
653                annot.setReadPermission(actor);
654            }
655        }
656
657        /*
658         * tags
659         */
660        if (jo.has("tags")) {
661            HashSet<String> tagset = new HashSet<String>();
662            JSONArray tags = jo.getJSONArray("tags");
663            for (int i = 0; i < tags.length(); ++i) {
664                tagset.add(tags.getString(i));
665            }
666            annot.setTags(tagset);
667        }
668
669        return annot;
670    }
671
672    @SuppressWarnings("unused")
673    // i in for loop
674    protected Actor getActorFromPermissions(JSONArray perms) throws JSONException {
675        Actor actor = null;
676        for (int i = 0; i < perms.length(); ++i) {
677            String perm = perms.getString(i);
678            if (perm.toLowerCase().startsWith("group:")) {
679                String groupId = perm.substring(6);
680                actor = new Group(groupId);
681            } else {
682                actor = new Person(perm);
683            }
684            // we just take the first one
685            break;
686        }
687        return actor;
688    }
689
690    public static float getFloat(String s) {
691        try {
692            return Float.parseFloat(s);
693        } catch (NumberFormatException e) {
694        }
695        return 0f;
696    }
697
698    public static int getInt(String s) {
699        try {
700            return Integer.parseInt(s);
701        } catch (NumberFormatException e) {
702        }
703        return 0;
704    }
705}
Note: See TracBrowser for help on using the repository browser.