view src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 102:9140017e8962

fix bug with empty username. add logging for JSON exceptions.
author casties
date Thu, 09 Feb 2017 20:46:15 +0100
parents acd44dfec9c8
children 7417f5915181
line wrap: on
line source

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
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #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.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<Header> requestHeaders = (Series<Header>) 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<Verifier> verifiers = new ArrayList<Verifier>();
            // 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<String> groups = new HashSet<String>();
                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<String> fragments = new ArrayList<String>();
                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<String> 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<String> 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<String> 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")) {
            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<String> tagset = new HashSet<String>();
            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;
    }
}