changeset 3:47b53ae385d1

merging old code
author casties
date Fri, 29 Jun 2012 20:38:27 +0200
parents f2d44c41eedf
children 3599b29c393f
files .hgignore pom.xml src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorAnnotations.java src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java src/main/java/de/mpiwg/itgroup/annotations/restlet/RestServer.java src/main/resources/de/itgroup/annotations/annotator-info.html src/main/resources/de/mpiwg/itgroup/annotations/annotator-info.html src/main/webapp/WEB-INF/web.xml
diffstat 8 files changed, 969 insertions(+), 55 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Jun 28 19:34:32 2012 +0200
+++ b/.hgignore	Fri Jun 29 20:38:27 2012 +0200
@@ -6,4 +6,6 @@
 syntax: regexp
 ^\.classpath$
 syntax: regexp
-^\.project$
\ No newline at end of file
+^\.project$
+syntax: regexp
+^\.shell_history$
\ No newline at end of file
--- a/pom.xml	Thu Jun 28 19:34:32 2012 +0200
+++ b/pom.xml	Fri Jun 29 20:38:27 2012 +0200
@@ -59,11 +59,27 @@
 			<artifactId>org.restlet.ext.json</artifactId>
 			<version>${restlet-version}</version>
 		</dependency>
+		<dependency>
+			<groupId>com.googlecode.jsontoken</groupId>
+			<artifactId>jsontoken</artifactId>
+			<version>1.1-SNAPSHOT</version>
+			<exclusions>
+				<exclusion>
+					<artifactId>servlet-api</artifactId>
+					<groupId>javax.servlet</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
         <dependency>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
             <version>1.2.14</version>
         </dependency>
+        <dependency>
+        	<groupId>commons-codec</groupId>
+        	<artifactId>commons-codec</artifactId>
+        	<version>1.4</version>
+        </dependency>
 	</dependencies>
 	<build>
 		<pluginManagement>
@@ -79,4 +95,5 @@
 			</plugins>
 		</pluginManagement>
 	</build>
+	<packaging>war</packaging>
 </project>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorAnnotations.java	Fri Jun 29 20:38:27 2012 +0200
@@ -0,0 +1,398 @@
+/**
+ * Implements the "annotations" uri of the Annotator API. see
+ * <https://github.com/okfn/annotator/wiki/Storage>
+ */
+package de.mpiwg.itgroup.annotations.restlet;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.restlet.Context;
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.ext.json.JsonRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.resource.Delete;
+import org.restlet.resource.Get;
+import org.restlet.resource.Post;
+import org.restlet.resource.Put;
+import org.restlet.security.User;
+
+import de.mpiwg.itgroup.annotationManager.Constants.NS;
+import de.mpiwg.itgroup.annotationManager.Errors.TripleStoreSearchError;
+import de.mpiwg.itgroup.annotationManager.Errors.TripleStoreStoreError;
+import de.mpiwg.itgroup.annotationManager.RDFHandling.Annotation;
+import de.mpiwg.itgroup.annotationManager.RDFHandling.Convert;
+import de.mpiwg.itgroup.annotationManager.RDFHandling.RDFSearcher;
+import de.mpiwg.itgroup.annotationManager.drupal.AnnotationHandler;
+import de.mpiwg.itgroup.annotationManager.drupal.UnknowUserException;
+import de.mpiwg.itgroup.triplestoremanager.exceptions.TripleStoreHandlerException;
+
+/**
+ * Implements the "annotations" uri of the Annotator API. see <https://github.com/okfn/annotator/wiki/Storage>
+ * 
+ * @author dwinter, casties
+ * 
+ */
+public class AnnotatorAnnotations extends AnnotatorResourceImpl {
+
+    protected String getAllowedMethodsForHeader() {
+        return "OPTIONS,GET,POST,PUT,DELETE";
+    }
+
+    /**
+     * GET with JSON content-type.
+     * 
+     * @param entity
+     * @return
+     */
+    @Get("json")
+    public Representation doGetJSON(Representation entity) {
+        logger.debug("AnnotatorAnnotations doGetJSON!");
+        setCorsHeaders();
+        // id from URI /annotations/{id}
+        String jsonId = (String) getRequest().getAttributes().get("id");
+        String id = decodeJsonId(jsonId);
+        logger.debug("annotation-id=" + id);
+
+        // TODO: what to return without id - list of all annotations?
+
+        // TODO: what to do with authentication?
+        boolean authenticated = isAuthenticated(entity);
+        logger.debug("request authenticated=" + authenticated);
+
+        RDFSearcher searcher = new RDFSearcher(NS.MPIWG_ANNOT_CTX); // TODO should ge into config file
+
+        try {
+            List<Annotation> annots = searcher.searchById(id);
+            if (annots.size() == 1) {
+                // there should be only one
+                JSONObject result = createAnnotatorJson(annots.get(0));
+                logger.debug("sending:");
+                logger.debug(result);
+                return new JsonRepresentation(result);
+            } else {
+                JSONArray results;
+                results = new JSONArray();
+                for (Annotation annot : annots) {
+                    JSONObject jo = createAnnotatorJson(annot);
+                    if (jo != null) {
+                        results.put(createAnnotatorJson(annot));
+                    } else {
+                        setStatus(Status.SERVER_ERROR_INTERNAL, "JSon Error");
+                        return null;
+                    }
+                }
+                // annotator read request returns a list of annotation objects
+                logger.debug("sending:");
+                logger.debug(results);
+                return new JsonRepresentation(results);
+            }
+        } catch (TripleStoreHandlerException e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStoreHandler Error");
+            return null;
+        } catch (TripleStoreSearchError e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStoreSearch Error");
+            return null;
+        }
+    }
+
+    /**
+     * POST with JSON content-type.
+     * 
+     * json hash: username: name des users xpointer: xpointer auf den Ausschnitt (incl. der URL des Dokumentes) text: text der
+     * annotation annoturl: url auf eine Annotation falls extern
+     * 
+     * @return
+     */
+    @Post("json")
+    public Representation doPostJson(Representation entity) {
+        logger.debug("AnnotatorAnnotations doPostJSON!");
+        // set headers
+        setCorsHeaders();
+        Annotation annot = null;
+        try {
+            JsonRepresentation jrep = new JsonRepresentation(entity);
+            JSONObject jo = jrep.getJsonObject();
+            if (jo == null) {
+                setStatus(Status.SERVER_ERROR_INTERNAL);
+                return null;
+            }
+            // make sure id is not set for POST
+            jo.remove("id");
+            // get Annotation object from posted JSON
+            annot = createAnnotation(jo, entity);
+        } catch (IOException e1) {
+            setStatus(Status.SERVER_ERROR_INTERNAL);
+            return null;
+        } catch (JSONException e) {
+            setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+            return null;
+        }
+        if (annot == null || annot.xpointer == null || annot.creator == null) {
+            setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+            return null;
+        }
+        Annotation storedAnnot;
+        try {
+            // store Annotation
+            storedAnnot = new Convert(NS.MPIWG_ANNOT_CTX).storeAnnotation(annot);
+        } catch (TripleStoreStoreError e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStore Error");
+            return null;
+        }
+        /* according to https://github.com/okfn/annotator/wiki/Storage
+         * we should return 303: see other.
+         * For now we return the annotation.
+         */
+        JSONObject jo = createAnnotatorJson(storedAnnot);
+        JsonRepresentation retRep = new JsonRepresentation(jo);
+        return retRep;
+    }
+
+    /**
+     * POST with HTML content-type.
+     * 
+     * @param entity
+     * @return
+     */
+    @Post("html")
+    public Representation doPostHtml(Representation entity) {
+        logger.debug("AnnotatorAnnotations doPostHtml!");
+        Annotation annot;
+        annot = handleForm(entity);
+        if (annot.xpointer == null || annot.creator == null) {
+            setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+
+            return null;
+        }
+
+        Annotation retValAnnot;
+        try {
+            retValAnnot = new Convert(NS.MPIWG_ANNOT_CTX).storeAnnotation(annot);
+        } catch (TripleStoreStoreError e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStore Error");
+            return null;
+        }
+        if (retValAnnot == null) {
+            return null;
+        }
+        String retVal = retValAnnot.getAnnotationUri();
+        if (retVal == null) {
+            return null;
+        }
+
+        String text = String.format("<html><body><a href=\"%s\">%s</a></body></html>", retVal.replace(">", "").replace("<", ""),
+                retVal.replace(">", "&gt;").replace("<", "&lt;"));
+        Representation retRep = new StringRepresentation(text, MediaType.TEXT_HTML);
+        return retRep;
+    }
+
+    /**
+     * 
+     * @param entity
+     *            should contain a form with the parameters "username", "password", "xpointer","text","uri","type"
+     * 
+     *            username,password is optional, if not given BasicAuthentification is used.
+     * 
+     *            If username given as a URI, the username will be transformed to an URI, username will be added to the MPIWG
+     *            namespace defined in de.mpiwg.itgroup.annotationManager.Constants.NS
+     * 
+     * @return
+     */
+    protected Annotation handleForm(Representation entity) {
+        Annotation annot;
+        Form form = new Form(entity);
+        String username = form.getValues("username");
+        String mode = form.getValues("mode");
+        String password = form.getValues("password");
+        String xpointer = form.getValues("xpointer");
+        String text = form.getValues("text");
+        String title = form.getValues("title");
+        String url = form.getValues("url");
+        String type = form.getValues("type");
+        RestServer restServer = (RestServer) getApplication();
+
+        // falls user and password nicht null sind:
+        User userFromForm = null;
+        if (username != null && password != null) {
+            if (restServer.authenticate(username, password, getRequest())) {
+                userFromForm = new User(username);
+            }
+        }
+        User authUser = null;
+
+        if (userFromForm == null) {
+            authUser = getHttpAuthUser(entity);
+        }
+
+        // weder BasicAuth noch FormAuth
+        if (authUser == null && userFromForm == null) {
+            setStatus(Status.CLIENT_ERROR_FORBIDDEN);
+            return null;
+        }
+
+        if (userFromForm != null) {
+            username = userFromForm.getIdentifier();
+        } else {
+            username = authUser.getIdentifier();
+        }
+
+        // username should be a URI, if not it will set to the MPIWG namespace defined in
+        // de.mpiwg.itgroup.annotationManager.Constants.NS
+        String usernameOrig = username;
+        if (!username.startsWith("http"))
+            username = NS.MPIWG_PERSONS_URL + username;
+
+        if (mode.equals("complexAnnotation")) {// Annotation mit text in externer ressource
+
+            Context context = getContext();
+            String drupalPath = context.getParameters().getFirstValue("de.mpiwg.itgroup.annotationManager.drupalServer");
+
+            AnnotationHandler ah = new AnnotationHandler(drupalPath);
+            JSONObject newAnnot;
+            try {
+                newAnnot = ah.createAnnotation(title, text, usernameOrig, password);
+            } catch (UnknowUserException e1) {
+                setStatus(Status.CLIENT_ERROR_FORBIDDEN);
+                e1.printStackTrace();
+                return null;
+            }
+            try {
+                annot = new Annotation(xpointer, username, null, text, type, newAnnot.getString("node_uri"));
+            } catch (JSONException e) {
+                // TODO Auto-generated catch block
+                e.printStackTrace();
+                setStatus(Status.SERVER_ERROR_INTERNAL);
+                return null;
+            }
+        } else
+            annot = new Annotation(xpointer, username, null, text, type, url);
+        return annot;
+    }
+
+
+    /**
+     * PUT with JSON content-type.
+     * 
+     * @param entity
+     * @return
+     */
+    @Put("json")
+    public Representation doPutJSON(Representation entity) {
+        logger.debug("AnnotatorAnnotations doPutJSON!");
+        setCorsHeaders();
+        // id from URI /annotations/{id}
+        String jsonId = (String) getRequest().getAttributes().get("id");
+        String id = decodeJsonId(jsonId);
+        logger.debug("annotation-id=" + id);
+
+        // TODO: what to do with authentication? we should check the owner
+        boolean authenticated = isAuthenticated(entity);
+        logger.debug("request authenticated=" + authenticated);
+        if (!authenticated) {
+            setStatus(Status.CLIENT_ERROR_FORBIDDEN, "Not Authorized!");
+            return null;            
+        }
+
+        Annotation annot = null;
+        try {
+            JsonRepresentation jrep = new JsonRepresentation(entity);
+            JSONObject jo = jrep.getJsonObject();
+            if (jo == null) {
+                setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+                return null;
+            }
+            RDFSearcher searcher = new RDFSearcher(NS.MPIWG_ANNOT_CTX); // TODO should ge into config file
+            // get stored Annotation
+            List<Annotation> annots = searcher.searchById(id);
+            if (annots.size() < 1) {
+                setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+                return null;
+            }
+            Annotation storedAnnot = annots.get(0);
+            // delete
+            searcher.deleteById(id);
+            // update from posted JSON
+            annot = updateAnnotation(storedAnnot, jo, entity);
+            // store Annotation
+            storedAnnot = new Convert(NS.MPIWG_ANNOT_CTX).storeAnnotation(annot);
+            /* according to https://github.com/okfn/annotator/wiki/Storage
+             * we should return 303: see other.
+             * but the client doesn't like it
+            setStatus(Status.REDIRECTION_SEE_OTHER);
+            // go to same URL as this one
+            Reference thisUrl = this.getReference();
+            this.getResponse().setLocationRef(thisUrl); */
+            // return new annotation
+            jo = createAnnotatorJson(storedAnnot);
+            JsonRepresentation retRep = new JsonRepresentation(jo);
+            return retRep;
+        } catch (TripleStoreHandlerException e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStoreHandler Error");
+        } catch (TripleStoreSearchError e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStoreSearch Error");
+        } catch (JSONException e) {
+            e.printStackTrace();
+            setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
+        } catch (IOException e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "Other Error");
+        }
+        return null;
+    }
+
+    /**
+     * DELETE with JSON content-type.
+     * 
+     * @param entity
+     * @return
+     */
+    @Delete("json")
+    public Representation doDeleteJSON(Representation entity) {
+        logger.debug("AnnotatorAnnotations doDeleteJSON!");
+        setCorsHeaders();
+        // id from URI /annotations/{id}
+        String jsonId = (String) getRequest().getAttributes().get("id");
+        String id = decodeJsonId(jsonId);
+        logger.debug("annotation-id=" + id);
+
+        // TODO: what to do with authentication? we should check the owner
+        boolean authenticated = isAuthenticated(entity);
+        logger.debug("request authenticated=" + authenticated);
+        if (!authenticated) {
+            setStatus(Status.CLIENT_ERROR_FORBIDDEN, "Not Authorized!");
+            return null;            
+        }
+        
+        RDFSearcher searcher = new RDFSearcher(NS.MPIWG_ANNOT_CTX); // TODO should ge into config file
+
+        try {
+            // delete annotation
+            searcher.deleteById(id);
+            setStatus(Status.SUCCESS_NO_CONTENT);
+        } catch (TripleStoreHandlerException e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStoreHandler Error");
+        } catch (TripleStoreSearchError e) {
+            e.printStackTrace();
+            setStatus(Status.SERVER_ERROR_INTERNAL, "TripleStoreSearch Error");
+        }
+        return null;
+    }
+    
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java	Fri Jun 29 20:38:27 2012 +0200
@@ -0,0 +1,443 @@
+/**
+ * Base class for Annotator resource classes.
+ */
+package de.mpiwg.itgroup.annotations.restlet;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.SignatureException;
+import java.util.ArrayList;
+import java.util.List;
+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.ClientInfo;
+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 org.restlet.security.User;
+
+import de.mpiwg.itgroup.annotationManager.Constants.NS;
+import de.mpiwg.itgroup.annotationManager.RDFHandling.Annotation;
+
+/**
+ * Base class for Annotator resource classes.
+ * 
+ * @author dwinter, casties
+ * 
+ */
+public abstract class AnnotatorResourceImpl extends ServerResource {
+
+    protected Logger logger = Logger.getRootLogger();
+
+    protected String getAllowedMethodsForHeader() {
+        return "OPTIONS,GET,POST";
+    }
+
+    public String encodeJsonId(String id) {
+        try {
+            return Base64.encodeBase64URLSafeString(id.getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+    }
+
+    public String decodeJsonId(String id) {
+        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.
+     * 
+     * @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);
+        // 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
+        RestServer restServer = (RestServer) 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
+     * @return
+     */
+    public JSONObject createAnnotatorJson(Annotation annot) {
+        boolean makeUserObject = true;
+        JSONObject jo = new JSONObject();
+        try {
+            jo.put("text", annot.text);
+            jo.put("uri", annot.url);
+
+            if (makeUserObject) {
+                // create user object
+                JSONObject userObject = new JSONObject();
+                // save creator as uri
+                userObject.put("uri", annot.creator);
+                // make short user id
+                String userID = annot.creator;
+                if (userID.startsWith(NS.MPIWG_PERSONS_URL)) {
+                    userID = userID.replace(NS.MPIWG_PERSONS_URL, ""); // entferne
+                                                                       // NAMESPACE
+                }
+                // save as id
+                userObject.put("id", userID);
+                // get full name
+                RestServer restServer = (RestServer) getApplication();
+                String userName = restServer.getUserNameFromLdap(userID);
+                userObject.put("name", userName);
+                // save user object
+                jo.put("user", userObject);
+            } else {
+                // save user as string
+                jo.put("user", annot.creator);
+            }
+
+            List<String> xpointers = new ArrayList<String>();
+            if (annot.xpointers == null || annot.xpointers.size() == 0)
+                xpointers.add(annot.xpointer);
+            else {
+                for (String xpointerString : annot.xpointers) {
+                    xpointers.add(xpointerString);
+                }
+            }
+            if (!xpointers.isEmpty()) {
+                // we only look at the first xpointer
+                String xt = getXpointerType(xpointers.get(0));
+                if (xt == "range") {
+                    jo.put("ranges", transformToRanges(xpointers));
+                } else if (xt == "area") {
+                    jo.put("areas", transformToAreas(xpointers));
+                }
+            }
+            // encode Annotation URL (=id) in base64
+            String annotUrl = annot.getAnnotationUri();
+            String annotId = encodeJsonId(annotUrl);
+            jo.put("id", annotId);
+            return jo;
+        } catch (JSONException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    private String getXpointerType(String xpointer) {
+        if (xpointer.contains("#xpointer")) {
+            return "range";
+        } else if (xpointer.contains("#xywh")) {
+            return "area";
+        }
+        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");
+                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();
+        } catch (UnsupportedEncodingException 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");
+                Matcher m = rg.matcher(decoded);
+
+                if (m.find()) {
+                    {
+                        JSONObject jo = new JSONObject();
+                        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();
+        } catch (UnsupportedEncodingException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+
+        return ja;
+    }
+
+    /**
+     * 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
+     */
+    public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException {
+        return updateAnnotation(new Annotation(), jo, entity);
+    }
+
+    public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException {
+        // annotated uri
+        String url = annot.url;
+        if (jo.has("uri")) {
+            url = jo.getString("uri");
+        }
+        // annotation text
+        String text = annot.text;
+        if (jo.has("text")) {
+            text = 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();
+        }
+        // username not required, if no username given authuser will be used
+        String username = null;
+        String userUri = annot.creator;
+        if (jo.has("user")) {
+            if (jo.get("user") instanceof String) {
+                // user is just a String
+                username = jo.getString("user");
+                // TODO: what if username and authUser are different?
+            } else {
+                // user is an object
+                JSONObject user = jo.getJSONObject("user");
+                if (user.has("id")) {
+                    username = user.getString("id");
+                }
+                if (user.has("uri")) {
+                    userUri = user.getString("uri");
+                }
+            }
+        }
+        if (username == null) {
+            username = authUser;
+        }
+        // username should be a URI, if not it will set to the MPIWG namespace
+        // defined in
+        // de.mpiwg.itgroup.annotationManager.Constants.NS
+        if (userUri == null) {
+            if (username.startsWith("http")) {
+                userUri = username;
+            } else {
+                userUri = NS.MPIWG_PERSONS_URL + username;
+            }
+        }
+        // TODO: should we overwrite the creator?
+
+        // create xpointer from the first range/area
+        String xpointer = annot.xpointer;
+        if (jo.has("ranges")) {
+            JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
+            String start = ranges.getString("start");
+            String end = ranges.getString("end");
+            String startOffset = ranges.getString("startOffset");
+            String endOffset = ranges.getString("endOffset");
+
+            try {
+                xpointer = url
+                        + "#"
+                        + URLEncoder.encode(String.format(
+                                "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))",
+                                start, startOffset, end, endOffset), "utf-8");
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+                setStatus(Status.SERVER_ERROR_INTERNAL);
+                return null;
+            }
+        }
+        if (jo.has("areas")) {
+            JSONObject area = jo.getJSONArray("areas").getJSONObject(0);
+            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");
+            }
+            try {
+                xpointer = url + "#" + URLEncoder.encode(String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height), "utf-8");
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+                setStatus(Status.SERVER_ERROR_INTERNAL);
+                return null;
+            }
+        }
+        return new Annotation(xpointer, userUri, annot.time, text, annot.type);
+    }
+
+    /**
+     * returns the logged in User.
+     * 
+     * @param entity
+     * @return
+     */
+    protected User getHttpAuthUser(Representation entity) {
+        RestServer restServer = (RestServer) getApplication();
+        if (!restServer.authenticate(getRequest(), getResponse())) {
+            // Not authenticated
+            return null;
+        }
+
+        ClientInfo ci = getRequest().getClientInfo();
+        logger.debug(ci);
+        return getRequest().getClientInfo().getUser();
+
+    }
+
+}
--- a/src/main/java/de/mpiwg/itgroup/annotations/restlet/RestServer.java	Thu Jun 28 19:34:32 2012 +0200
+++ b/src/main/java/de/mpiwg/itgroup/annotations/restlet/RestServer.java	Fri Jun 29 20:38:27 2012 +0200
@@ -19,6 +19,10 @@
 
 import org.apache.log4j.BasicConfigurator;
 import org.apache.log4j.Logger;
+import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.graphdb.factory.GraphDatabaseFactory;
+import org.neo4j.kernel.AbstractGraphDatabase;
+import org.neo4j.server.WrappingNeoServerBootstrapper;
 import org.restlet.Application;
 import org.restlet.Context;
 import org.restlet.Restlet;
@@ -30,14 +34,18 @@
 public class RestServer extends Application {
 
     public static Logger logger = Logger.getRootLogger();
-    
+
     private ChallengeAuthenticator authenticator;
 
     /**
      * Properties holding consumer keys and secrets
      */
     private Properties consumerKeys;
-    public final String CONSUMER_KEYS_PATH = "WEB-INF/consumerkeys.property";
+    public String CONSUMER_KEYS_PATH = "WEB-INF/consumerkeys.property";
+
+    private GraphDatabaseService graphDb;
+    public static final String GRAPHDB_KEY = "annotationmanager.graphdb";
+    public String DB_PATH = "WEB-INF/neo4j-annotation-db";
 
     /**
      * constructor
@@ -46,50 +54,58 @@
      */
     public RestServer(Context parentContext) {
         super(parentContext);
-
-        Logger rl = Logger.getRootLogger();
+        // TODO: is this the right place to run the log4j configurator?
         BasicConfigurator.configure();
-        // read consumerKeys from webapp
-        consumerKeys = new Properties();
-        ServletContext sc = (ServletContext) getContext().getServerDispatcher()
-                .getContext().getAttributes()
+
+        ServletContext sc = (ServletContext) getContext().getServerDispatcher().getContext().getAttributes()
                 .get("org.restlet.ext.servlet.ServletContext");
         if (sc != null) {
-            InputStream ps = sc.getResourceAsStream(CONSUMER_KEYS_PATH);
-            if (ps == null) {
-                // try as file
-                File pf = new File(sc.getRealPath(CONSUMER_KEYS_PATH));
-                if (pf != null) {
-                    rl.debug("trying file for consumer keys: "+pf);
-                    try {
-                        ps = new FileInputStream(pf);
-                    } catch (FileNotFoundException e) {
-                        // TODO Auto-generated catch block
-                        e.printStackTrace();
-                    }
+            // look for database service in context
+            graphDb = (GraphDatabaseService) sc.getAttribute(GRAPHDB_KEY);
+            if (graphDb == null) {
+                /*
+                 * open database
+                 */
+                String dbFn = getResourcePath(sc, DB_PATH);
+                if (dbFn != null) {
+                    logger.debug("opening DB " + dbFn);
+                    graphDb = new GraphDatabaseFactory().newEmbeddedDatabase(dbFn);
+                    // store in context
+                    sc.setAttribute(GRAPHDB_KEY, graphDb);
+                    WrappingNeoServerBootstrapper srv = new WrappingNeoServerBootstrapper((AbstractGraphDatabase) graphDb);
+                    logger.debug("Starting DB admin server...");
+                    srv.start();
+                } else {
+                    logger.error("Unable to get resource " + DB_PATH);
                 }
             }
+            /*
+             * read consumerKeys from webapp
+             */
+            consumerKeys = new Properties();
+            InputStream ps = getResourceAsStream(sc, CONSUMER_KEYS_PATH);
             if (ps != null) {
-                rl.debug("loading consumer keys from "+CONSUMER_KEYS_PATH);
+                logger.debug("loading consumer keys from " + CONSUMER_KEYS_PATH);
                 try {
                     consumerKeys.load(ps);
                 } catch (IOException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                 }
-                rl.debug("consumer keys: "+consumerKeys);
+                logger.debug("consumer keys: " + consumerKeys);
             } else {
-                rl.error("Unable to get resource "+CONSUMER_KEYS_PATH);
+                logger.error("Unable to get resource " + CONSUMER_KEYS_PATH);
             }
         } else {
-            rl.error("Unable to get ServletContext!");
+            logger.error("Unable to get ServletContext!");
         }
-        
+
     }
-    
+
     /**
-     * returns consumer secret for consumer key.
-     * returns null if consumer key doesn't exist.
+     * returns consumer secret for consumer key. returns null if consumer key
+     * doesn't exist.
+     * 
      * @param consumerKey
      * @return
      */
@@ -97,7 +113,6 @@
         return consumerKeys.getProperty(consumerKey);
     }
 
-
     /*
      * (non-Javadoc)
      * 
@@ -105,26 +120,21 @@
      */
     @Override
     public Restlet createInboundRoot() {
-        //this.authenticator = createAuthenticator();
-
-        // String target = "{rh}/{rf}/XX";
-        // Redirector redirector = new
-        // Redirector(getContext().createChildContext(), target,
-        // Redirector.MODE_CLIENT_SEE_OTHER);
+        // this.authenticator = createAuthenticator();
 
         Router router = new Router(getContext());
-        /*
+
         router.attach("/annotator/annotations", AnnotatorAnnotations.class);
         router.attach("/annotator/annotations/{id}", AnnotatorAnnotations.class);
-        router.attach("/annotator/search", AnnotatorSearch.class);
-        
-        // router.attach("",redirector);
-        router.attach("/annotator", ExtendedAnnotationInput.class);
-        */
+        // router.attach("/annotator/search", AnnotatorSearch.class);
+
+        // router.attach("",redirector); router.attach("/annotator",
+        // ExtendedAnnotationInput.class);
+
         router.attach("/", AnnotatorInfo.class);
-        //authenticator.setNext(router);
-        //return authenticator;
-        
+        // authenticator.setNext(router);
+        // return authenticator;
+
         return router;
     }
 
@@ -141,7 +151,8 @@
         String sp = "com.sun.jndi.ldap.LdapCtxFactory";
         env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, sp);
 
-        String ldapUrl = "ldap://ldapreplik.mpiwg-berlin.mpg.de/dc=mpiwg-berlin,dc=mpg,dc=de"; // TODO should go into config file
+        // TODO: should go into config file
+        String ldapUrl = "ldap://ldapreplik.mpiwg-berlin.mpg.de/dc=mpiwg-berlin,dc=mpg,dc=de"; 
         env.put(javax.naming.Context.PROVIDER_URL, ldapUrl);
 
         DirContext dctx;
@@ -163,8 +174,7 @@
         String filter = "(uid=" + creator + ")";
 
         try {
-            NamingEnumeration<SearchResult> results = dctx.search(base, filter,
-                    sc);
+            NamingEnumeration<SearchResult> results = dctx.search(base, filter, sc);
             while (results.hasMore()) {
                 SearchResult sr = (SearchResult) results.next();
                 javax.naming.directory.Attributes attrs = sr.getAttributes();
@@ -186,4 +196,48 @@
         return retString;
     }
 
+    /**
+     * returns resource from path (in webapp) as InputStream.
+     * 
+     * @param sc
+     * @param path
+     * @return
+     */
+    protected InputStream getResourceAsStream(ServletContext sc, String path) {
+        InputStream ps = sc.getResourceAsStream(path);
+        if (ps == null) {
+            // try as file
+            File pf = new File(sc.getRealPath(path));
+            if (pf != null) {
+                logger.debug("trying file for: " + pf);
+                try {
+                    ps = new FileInputStream(pf);
+                } catch (FileNotFoundException e) {
+                    logger.error(e);
+                }
+            }
+        }
+        return ps;
+    }
+
+    /**
+     * get a real file name for a web app file pathname.
+     * 
+     * If filename starts with "/" its treated as absolute else the path is
+     * appended to the base directory of the web-app.
+     * 
+     * @param filename
+     * @param sc
+     * @return
+     */
+    public static String getResourcePath(ServletContext sc, String filename) {
+        File f = new File(filename);
+        // is the filename absolute?
+        if (!f.isAbsolute()) {
+            // relative path -> use getRealPath to resolve in webapp
+            filename = sc.getRealPath(filename);
+        }
+        return filename;
+    }
+
 }
--- a/src/main/resources/de/itgroup/annotations/annotator-info.html	Thu Jun 28 19:34:32 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<html>
-<body>
-<h2>Annotation manager</h2>
-<p><a href="annotations">Annotations overview</a></p>
-</body>
-</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/resources/de/mpiwg/itgroup/annotations/annotator-info.html	Fri Jun 29 20:38:27 2012 +0200
@@ -0,0 +1,6 @@
+<html>
+<body>
+<h2>Annotation manager</h2>
+<p><a href="annotations">Annotations overview</a></p>
+</body>
+</html>
\ No newline at end of file
--- a/src/main/webapp/WEB-INF/web.xml	Thu Jun 28 19:34:32 2012 +0200
+++ b/src/main/webapp/WEB-INF/web.xml	Fri Jun 29 20:38:27 2012 +0200
@@ -4,7 +4,7 @@
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
 http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
-    <display-name>RESTfulJavaWebServices-Restlet</display-name>
+    <display-name>AnnotationManager-Restlet</display-name>
     <!-- Application classname -->
     <context-param>
         <param-name>org.restlet.application</param-name>