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

Last change on this file since 76:4e2dc67997a0 was 76:4e2dc67997a0, checked in by casties, 10 years ago

save text quote from Annotator.

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