package de.mpiwg.itgroup.annotations.restlet; /* * #%L * AnnotationManager * %% * Copyright (C) 2012 - 2014 MPIWG Berlin * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Lesser Public License for more details. * * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * . * #L% */ import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.oauth.jsontoken.Checker; import net.oauth.jsontoken.JsonToken; import net.oauth.jsontoken.JsonTokenParser; import net.oauth.jsontoken.SystemClock; import net.oauth.jsontoken.crypto.HmacSHA256Verifier; import net.oauth.jsontoken.crypto.Verifier; import org.apache.commons.codec.binary.Base64; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.restlet.data.Header; import org.restlet.data.Status; import org.restlet.representation.Representation; import org.restlet.resource.ServerResource; import org.restlet.util.Series; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import de.mpiwg.itgroup.annotations.Actor; import de.mpiwg.itgroup.annotations.Annotation; import de.mpiwg.itgroup.annotations.Annotation.Action; import de.mpiwg.itgroup.annotations.Annotation.FragmentTypes; import de.mpiwg.itgroup.annotations.Group; import de.mpiwg.itgroup.annotations.Person; import de.mpiwg.itgroup.annotations.Resource; import de.mpiwg.itgroup.annotations.Target; import de.mpiwg.itgroup.annotations.neo4j.AnnotationStore; /** * Base class for Annotator resource classes. * * @author dwinter, casties * */ public abstract class AnnotatorResourceImpl extends ServerResource { protected static Logger logger = Logger.getLogger(AnnotatorResourceImpl.class.getCanonicalName()); private AnnotationStore store; protected AnnotationStore getAnnotationStore() { if (store == null) { store = ((BaseRestlet) getApplication()).getAnnotationStore(); } return store; } public String encodeJsonId(String id) { if (id == null) return null; try { return Base64.encodeBase64URLSafeString(id.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { return null; } } public String decodeJsonId(String id) { if (id == null) return null; try { return new String(Base64.decodeBase64(id), "UTF-8"); } catch (UnsupportedEncodingException e) { return null; } } /** * returns if authentication information from headers is valid. * * @param entity * @return */ public boolean isAuthenticated(Representation entity) { return (getUserFromAuthToken(entity) != null); } /** * Checks Annotator Auth plugin authentication information from headers. * Returns userId if successful. Returns "anonymous" in non-authorization * mode. * * @param entity * @return user-id */ public Person getUserFromAuthToken(Representation entity) { @SuppressWarnings("unchecked") Series
requestHeaders = (Series
) getRequest().getAttributes().get("org.restlet.http.headers"); String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true); if (authToken == null) { if (!((BaseRestlet) getApplication()).isAuthorizationMode()) { // no token, no-auth mode -> anonymous return Person.getAnonymous(); } // no token, auth mode -> null return null; } try { // decode token first to get consumer key JsonToken token = new JsonTokenParser(null, null).deserialize(authToken); String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString(); // get stored consumer secret for key BaseRestlet restServer = (BaseRestlet) getApplication(); String consumerSecret = restServer.getConsumerSecret(consumerKey); logger.fine("requested consumer key=" + consumerKey + " secret=" + consumerSecret); if (consumerSecret == null) { logger.warning("Error: unknown consumer key: "+consumerKey); return null; } // logger.fine(String.format("token=%s tokenString=%s signatureAlgorithm=%s",token,token.getTokenString(),token.getSignatureAlgorithm())); List verifiers = new ArrayList(); // we only do HS256 yet verifiers.add(new HmacSHA256Verifier(consumerSecret.getBytes("UTF-8"))); // verify token signature(should really be static...) new JsonTokenParser(new SystemClock(), null, (Checker[]) null).verify(token, verifiers); // create Person JsonObject payload = token.getPayloadAsJsonObject(); // userId is mandatory String userId = payload.get("userId").getAsString(); Person user = new Person(userId); // displayName is optional if (payload.has("displayName")) { user.name = payload.get("displayName").getAsString(); } // memberOf groups is optional if (payload.has("memberOf")) { Set groups = new HashSet(); JsonArray jgroups = payload.get("memberOf").getAsJsonArray(); for (JsonElement jgroup : jgroups) { groups.add(jgroup.getAsString()); } user.groups = groups; } logger.fine("auth OK! user=" + user); return user; } catch (Exception e) { logger.warning("Error checking auth token: "+e.toString()); } return null; } /** * creates Annotator-JSON from an Annotation object. * * @param annot annotation object * @param forAnonymous * @return Annotator-JSON */ public JSONObject createAnnotatorJson(Annotation annot, boolean forAnonymous) { // return user as a JSON object (otherwise just as string) boolean makeUserObject = true; JSONObject jo = new JSONObject(); try { jo.put("text", annot.getBodyText()); jo.put("uri", annot.getTargetBaseUri()); if (annot.getResourceUri() != null) { jo.put("resource", annot.getResourceUri()); } if (annot.getQuote() != null) { jo.put("quote", annot.getQuote()); } /* * user */ Actor creator = annot.getCreator(); if (creator != null) { if (makeUserObject) { // create user object JSONObject userObject = new JSONObject(); // save creator as uri userObject.put("uri", creator.getUri()); // make short user id String userId = creator.getIdString(); // set as id userObject.put("id", userId); // get full name String userName = creator.getName(); if (userName == null) { BaseRestlet restServer = (BaseRestlet) getApplication(); userName = restServer.getFullNameForId(userId); } userObject.put("name", userName); // save user object jo.put("user", userObject); } else { // save user as string jo.put("user", annot.getCreatorUri()); } } /* * ranges */ if (annot.getTargetFragment() != null) { // we only look at the first xpointer List fragments = new ArrayList(); fragments.add(annot.getTargetFragment()); FragmentTypes xt = annot.getFragmentType(); if (xt == FragmentTypes.XPOINTER) { jo.put("ranges", transformToRanges(fragments)); } else if (xt == FragmentTypes.AREA) { jo.put("shapes", transformToShapes(fragments)); } else if (xt == FragmentTypes.WKT) { jo.put("shapes", transformToShapes(fragments)); } } /* * permissions */ JSONObject perms = new JSONObject(); jo.put("permissions", perms); // admin JSONArray adminPerms = new JSONArray(); perms.put("admin", adminPerms); Actor adminPerm = annot.getAdminPermission(); if (adminPerm != null) { adminPerms.put(adminPerm.getIdString()); } else if (forAnonymous) { // set something because its not allowed for anonymous adminPerms.put("not-you"); } // delete JSONArray deletePerms = new JSONArray(); perms.put("delete", deletePerms); Actor deletePerm = annot.getDeletePermission(); if (deletePerm != null) { deletePerms.put(deletePerm.getIdString()); } else if (forAnonymous) { // set something because its not allowed for anonymous deletePerms.put("not-you"); } // update JSONArray updatePerms = new JSONArray(); perms.put("update", updatePerms); Actor updatePerm = annot.getUpdatePermission(); if (updatePerm != null) { updatePerms.put(updatePerm.getIdString()); } else if (forAnonymous) { // set something because its not allowed for anonymous updatePerms.put("not-you"); } // read JSONArray readPerms = new JSONArray(); perms.put("read", readPerms); Actor readPerm = annot.getReadPermission(); if (readPerm != null) { readPerms.put(readPerm.getIdString()); } /* * tags */ Set tagset = annot.getTags(); if (tagset != null) { JSONArray tags = new JSONArray(); jo.put("tags", tags); for (String tag : tagset) { tags.put(tag); } } /* * id */ // encode Annotation URL (=id) in base64 String annotUrl = annot.getUri(); String annotId = encodeJsonId(annotUrl); jo.put("id", annotId); return jo; } catch (JSONException e) { logger.severe("Unable to create AnnotatorJSON! "+e); } return null; } private JSONArray transformToRanges(List xpointers) { JSONArray ja = new JSONArray(); Pattern rg = Pattern .compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)"); Pattern rg1 = Pattern.compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)"); try { for (String xpointer : xpointers) { // String decoded = URLDecoder.decode(xpointer, "utf-8"); String decoded = xpointer; Matcher m = rg.matcher(decoded); if (m.find()) { JSONObject jo = new JSONObject(); jo.put("start", m.group(1)); jo.put("startOffset", m.group(2)); jo.put("end", m.group(3)); jo.put("endOffset", m.group(4)); ja.put(jo); } m = rg1.matcher(xpointer); if (m.find()) { JSONObject jo = new JSONObject(); jo.put("start", m.group(1)); jo.put("startOffset", m.group(2)); ja.put(jo); } } } catch (JSONException e) { logger.severe("Unable to transform to ranges! "+e); } return ja; } private JSONArray transformToShapes(List fragments) { JSONArray ja = new JSONArray(); Pattern xywhPattern = Pattern.compile("xywh=(\\w*):([\\d\\.]+),([\\d\\.]+),([\\d\\.]+),([\\d\\.]+)"); Pattern wktPattern = Pattern.compile("wkt=(\\w+)\\(+([\\d\\.\\,\\ ]+)\\)+"); try { for (String fragment : fragments) { Matcher xywhMatch = xywhPattern.matcher(fragment); Matcher wktMatch = wktPattern.matcher(fragment); if (xywhMatch.find()) { // xywh rectangle fragment String units = xywhMatch.group(1); float x = getFloat(xywhMatch.group(2)); float y = getFloat(xywhMatch.group(3)); float width = getFloat(xywhMatch.group(4)); float height = getFloat(xywhMatch.group(5)); JSONObject shape = new JSONObject(); JSONObject geom = new JSONObject(); geom.put("units", units); geom.put("x", x); geom.put("y", y); if (width == 0 || height == 0) { shape.put("type", "point"); shape.put("geometry", geom); } else { shape.put("type", "rectangle"); geom.put("width", width); geom.put("height", height); shape.put("geometry", geom); } ja.put(shape); } else if (wktMatch.find()) { // wkt shape fragment String type = wktMatch.group(1); String coordString = wktMatch.group(2); JSONObject shape = new JSONObject(); JSONObject geom = new JSONObject(); shape.put("type", type.toLowerCase()); // TODO: add units/crs to fragment? geom.put("units", "fraction"); JSONArray coords = new JSONArray(); String[] coordPairs = coordString.split(", *"); for (String coordPairString : coordPairs) { String[] coordPair = coordPairString.split(" +"); coords.put(new JSONArray(coordPair)); } geom.put("coordinates", coords); shape.put("geometry", geom); ja.put(shape); } } } catch (JSONException e) { logger.severe("Unable to transform to shapes! "+e); } return ja; } protected String parseShape(JSONObject shape) throws JSONException { String fragment = null; String type = shape.getString("type"); JSONObject geom = shape.getJSONObject("geometry"); if (type.equalsIgnoreCase("point")) { // point shape String x = geom.getString("x"); String y = geom.getString("y"); fragment = String.format("xywh=fraction:%s,%s,0,0", x, y); } else if (type.equalsIgnoreCase("rectangle")) { // rectangle shape String x = geom.getString("x"); String y = geom.getString("y"); String width = geom.getString("width"); String height = geom.getString("height"); fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height); } else if (type.equalsIgnoreCase("polygon")) { // polygon shape JSONArray coordArray = geom.getJSONArray("coordinates"); StringBuilder coords = new StringBuilder(); int numCoords = coordArray.length(); for (int i = 0; i < numCoords; ++i) { JSONArray coordPair = coordArray.getJSONArray(i); coords.append(coordPair.getString(0)); coords.append(" "); coords.append(coordPair.getString(1)); if (i < numCoords-1) { coords.append(", "); } } // TODO: add units/crs to wkt // assume polygon with outer ring fragment = String.format("wkt=POLYGON((%s))", coords); } else if (type.equalsIgnoreCase("linestring")) { // linestring (polyline) shape JSONArray coordArray = geom.getJSONArray("coordinates"); StringBuilder coords = new StringBuilder(); int numCoords = coordArray.length(); for (int i = 0; i < numCoords; ++i) { JSONArray coordPair = coordArray.getJSONArray(i); coords.append(coordPair.getString(0)); coords.append(" "); coords.append(coordPair.getString(1)); if (i < numCoords-1) { coords.append(", "); } } // TODO: add units/crs to wkt fragment = String.format("wkt=LINESTRING(%s)", coords); } else { logger.severe("Unable to parse this shape: " + shape); } return fragment; } protected String parseArea(JSONObject area) throws JSONException { String x = area.getString("x"); String y = area.getString("y"); String width = "0"; String height = "0"; if (area.has("width")) { width = area.getString("width"); height = area.getString("height"); } String fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height); return fragment; } protected String parseRange(JSONObject range) throws JSONException { String start = range.getString("start"); String end = range.getString("end"); String startOffset = range.getString("startOffset"); String endOffset = range.getString("endOffset"); String fragment = String.format( "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))", start, startOffset, end, endOffset); return fragment; } /** * Creates an Annotation object with data from JSON. * * uses the specification from the annotator project: {@link https * ://github.com/okfn/annotator/wiki/Annotation-format} * * The username will be transformed to an URI if not given already as URI, * if not it will set to the MPIWG namespace defined in * de.mpiwg.itgroup.annotationManager.Constants.NS * * @param jo * @return * @throws JSONException * @throws UnsupportedEncodingException */ public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException { return updateAnnotation(new Annotation(), jo, entity); } /** * Updates an Annotation object with data from JSON. * * uses the specification from the annotator project: {@link https * ://github.com/okfn/annotator/wiki/Annotation-format} * * The username will be transformed to an URI if not given already as URI, * if not it will set to the MPIWG namespace defined in * de.mpiwg.itgroup.annotationManager.Constants.NS * * @param annot * @param jo * @return * @throws JSONException * @throws UnsupportedEncodingException */ public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException { /* * target uri */ if (jo.has("uri")) { annot.setTarget(new Target(jo.getString("uri"))); } /* * resource uri */ if (jo.has("resource")) { annot.setResource(new Resource(jo.getString("resource"))); } /* * annotation text */ if (jo.has("text")) { annot.setBodyText(jo.getString("text")); } /* * annotation quote */ if (jo.has("quote")) { annot.setQuote(jo.getString("quote")); } /* * check authentication */ Person authUser = getUserFromAuthToken(entity); if (authUser == null) { /* * // try http auth User httpUser = getHttpAuthUser(entity); if * (httpUser == null) { */ setStatus(Status.CLIENT_ERROR_FORBIDDEN); return null; /* * } authUser = httpUser.getIdentifier(); */ } /* * get or create creator object */ Actor creator = annot.getCreator(); if (creator == null) { creator = new Person(); annot.setCreator(creator); } // username not required, if no username given authuser will be used String username = null; String userUri = creator.getUri(); if (jo.has("user")) { if (jo.get("user") instanceof String) { // user is just a String username = jo.getString("user"); creator.setId(username); // TODO: what if username and authUser are different? } else { // user is an object JSONObject user = jo.getJSONObject("user"); if (user.has("id")) { String id = user.getString("id"); creator.setId(id); username = id; } if (user.has("uri")) { userUri = user.getString("uri"); } } } if (username == null) { username = authUser.getName(); } // try to get full name if (creator.getName() == null && username != null) { BaseRestlet restServer = (BaseRestlet) getApplication(); String fullName = restServer.getFullNameForId(username); creator.setName(fullName); } // userUri should be a URI, if not it will set to the MPIWG namespace if (userUri == null) { if (username.startsWith("http")) { userUri = username; } else { userUri = BaseRestlet.PERSONS_URI_PREFIX + username; } } // TODO: should we overwrite the creator? if (creator.getUri() == null) { creator.setUri(userUri); } /* * creation date */ if (annot.getCreated() == null) { // new - set creation date SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); String ct = format.format(Calendar.getInstance().getTime()); annot.setCreated(ct); } else { /* * update date */ // not new - set last update date SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); String ct = format.format(Calendar.getInstance().getTime()); annot.setUpdated(ct); } /* * create fragment from the first range/area */ try { if (jo.has("ranges")) { JSONArray ranges = jo.getJSONArray("ranges"); if (ranges.length() > 0) { JSONObject range = ranges.getJSONObject(0); annot.setFragmentType(FragmentTypes.XPOINTER); String fragment = parseRange(range); annot.setTargetFragment(fragment); } } } catch (JSONException e) { logger.warning(e.toString()); } try { if (jo.has("shapes")) { JSONArray shapes = jo.getJSONArray("shapes"); if (shapes.length() > 0) { JSONObject shape = shapes.getJSONObject(0); String fragment = parseShape(shape); annot.setTargetFragment(fragment); if (fragment.startsWith("wkt=")) { annot.setFragmentType(FragmentTypes.WKT); } else { annot.setFragmentType(FragmentTypes.AREA); } } } } catch (JSONException e) { logger.warning(e.toString()); } // deprecated areas type try { if (jo.has("areas")) { JSONArray areas = jo.getJSONArray("areas"); if (areas.length() > 0) { JSONObject area = areas.getJSONObject(0); annot.setFragmentType(FragmentTypes.AREA); String fragment = parseArea(area); annot.setTargetFragment(fragment); } } } catch (JSONException e) { logger.warning(e.toString()); } // no fragment is an error if (annot.getFragmentType() == null || annot.getTargetFragment() == null) { throw new JSONException("Annotation has no valid target fragment!"); } /* * permissions */ if (jo.has("permissions")) { // change permissions only if user has admin permission if (annot.isActionAllowed(Action.admin, authUser, getAnnotationStore())) { JSONObject permissions = jo.getJSONObject("permissions"); if (permissions.has("admin")) { JSONArray perms = permissions.getJSONArray("admin"); Actor actor = getActorFromPermissions(perms); annot.setAdminPermission(actor); } if (permissions.has("delete")) { JSONArray perms = permissions.getJSONArray("delete"); Actor actor = getActorFromPermissions(perms); annot.setDeletePermission(actor); } if (permissions.has("update")) { JSONArray perms = permissions.getJSONArray("update"); Actor actor = getActorFromPermissions(perms); annot.setUpdatePermission(actor); } if (permissions.has("read")) { JSONArray perms = permissions.getJSONArray("read"); Actor actor = getActorFromPermissions(perms); annot.setReadPermission(actor); } } } /* * tags */ if (jo.has("tags")) { HashSet tagset = new HashSet(); JSONArray tags = jo.getJSONArray("tags"); for (int i = 0; i < tags.length(); ++i) { tagset.add(tags.getString(i)); } annot.setTags(tagset); } return annot; } @SuppressWarnings("unused") // i in for loop protected Actor getActorFromPermissions(JSONArray perms) throws JSONException { Actor actor = null; for (int i = 0; i < perms.length(); ++i) { String perm = perms.getString(i); if (perm.toLowerCase().startsWith("group:")) { String groupId = perm.substring(6); actor = new Group(groupId); } else { actor = new Person(perm); } // we just take the first one break; } return actor; } public static float getFloat(String s) { try { return Float.parseFloat(s); } catch (NumberFormatException e) { } return 0f; } public static int getInt(String s) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { } return 0; } }