view src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 58:f5c0e6df7e88

made uri prefixes in store configurable.
author casties
date Tue, 20 Nov 2012 18:23:52 +0100
parents 4efb21cf0ce0
children b8ef15c8c4a5
line wrap: on
line source

/**
 * Base class for Annotator resource classes.
 */
package de.mpiwg.itgroup.annotations.restlet;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.SignatureException;
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.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.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.data.Form;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.Options;
import org.restlet.resource.ServerResource;

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);

    private AnnotationStore store;

    protected String getAllowedMethodsForHeader() {
        return "OPTIONS,GET,POST";
    }

    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;
        }
    }

    /**
     * Handle options request to allow CORS for AJAX.
     * 
     * @param entity
     */
    @Options
    public void doOptions(Representation entity) {
        logger.debug("AnnotatorResourceImpl doOptions!");
        setCorsHeaders();
    }

    /**
     * set headers to allow CORS for AJAX.
     */
    protected void setCorsHeaders() {
        Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
        if (responseHeaders == null) {
            responseHeaders = new Form();
            getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
        }
        responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
        // echo back Origin and Request-Headers
        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
        String origin = requestHeaders.getFirstValue("Origin", true);
        if (origin == null) {
            responseHeaders.add("Access-Control-Allow-Origin", "*");
        } else {
            responseHeaders.add("Access-Control-Allow-Origin", origin);
        }
        String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
        if (allowHeaders != null) {
            responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
        }
        responseHeaders.add("Access-Control-Allow-Credentials", "true");
        responseHeaders.add("Access-Control-Max-Age", "60");
    }

    /**
     * returns if authentication information from headers is valid.
     * 
     * @param entity
     * @return
     */
    public boolean isAuthenticated(Representation entity) {
        return (checkAuthToken(entity) != null);
    }

    /**
     * Checks Annotator Auth plugin authentication information from headers.
     * Returns userId if successful. Returns "anonymous" in non-authorization mode.
     * 
     * @param entity
     * @return
     */
    public String checkAuthToken(Representation entity) {
        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
        String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
        if (authToken == null) {
            if (!((BaseRestlet) getApplication()).isAuthorizationMode()) {
                return "anonymous";
            }
            return null;
        }
        // decode token first to get consumer key
        JsonToken token = new JsonTokenParser(null, null).deserialize(authToken);
        String userId = token.getParamAsPrimitive("userId").getAsString();
        String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString();
        // get stored consumer secret for key
        BaseRestlet restServer = (BaseRestlet) getApplication();
        String consumerSecret = restServer.getConsumerSecret(consumerKey);
        logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
        if (consumerSecret == null) {
            return null;
        }
        // logger.debug(String.format("token=%s tokenString=%s signatureAlgorithm=%s",token,token.getTokenString(),token.getSignatureAlgorithm()));
        try {
            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);
        } catch (SignatureException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // must be ok then
        logger.debug("auth OK! user=" + userId);
        return userId;
    }

    /**
     * creates Annotator-JSON from an Annotation object.
     * 
     * @param annot
     * @param forAnonymous TODO
     * @return
     */
    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());
            }

            /*
             * user
             */
            if (makeUserObject) {
                // create user object
                JSONObject userObject = new JSONObject();
                Actor creator = annot.getCreator();
                // 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.getFullNameFromLdap(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("areas", transformToAreas(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) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        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) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return ja;
    }

    private JSONArray transformToAreas(List<String> xpointers) {

        JSONArray ja = new JSONArray();

        Pattern rg = Pattern.compile("xywh=(\\w*:)([\\d\\.]+),([\\d\\.]+),([\\d\\.]+),([\\d\\.]+)");

        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();
                        @SuppressWarnings("unused")
                        String unit = m.group(1);
                        jo.put("x", m.group(2));
                        jo.put("y", m.group(3));
                        jo.put("width", m.group(4));
                        jo.put("height", m.group(5));
                        ja.put(jo);
                    }
                }
            }
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return ja;
    }

    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"));
        }
        /*
         * check authentication
         */
        String authUser = checkAuthToken(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;
        }
        // try to get full name
        if (creator.getName() == null && username != null) {
            BaseRestlet restServer = (BaseRestlet) getApplication();
            String fullName = restServer.getFullNameFromLdap(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) {
            // 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);
        }

        /*
         * create xpointer from the first range/area
         */
        try {
            if (jo.has("ranges")) {
                JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
                annot.setFragmentType(FragmentTypes.XPOINTER);
                String fragment = parseRange(ranges);
                annot.setTargetFragment(fragment);
            }
        } catch (JSONException e) {
            // nothing to do
        }
        try {
            if (jo.has("areas")) {
                JSONObject area = jo.getJSONArray("areas").getJSONObject(0);
                annot.setFragmentType(FragmentTypes.AREA);
                String fragment = parseArea(area);
                annot.setTargetFragment(fragment);
            }
        } catch (JSONException e) {
            // nothing to do
        }

        /*
         * 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;
    }

}