# HG changeset patch # User casties # Date 1340995107 -7200 # Node ID 47b53ae385d1f9e3491a07bbab97d9f35ca494ec # Parent f2d44c41eedf54c1f42b8a8b3ad2b9cdc7da5033 merging old code diff -r f2d44c41eedf -r 47b53ae385d1 .hgignore --- 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 diff -r f2d44c41eedf -r 47b53ae385d1 pom.xml --- 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 @@ org.restlet.ext.json ${restlet-version} + + com.googlecode.jsontoken + jsontoken + 1.1-SNAPSHOT + + + servlet-api + javax.servlet + + + log4j log4j 1.2.14 + + commons-codec + commons-codec + 1.4 + @@ -79,4 +95,5 @@ + war \ No newline at end of file diff -r f2d44c41eedf -r 47b53ae385d1 src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorAnnotations.java --- /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 + * + */ +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 + * + * @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 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("%s", retVal.replace(">", "").replace("<", ""), + retVal.replace(">", ">").replace("<", "<")); + 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 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; + } + + +} diff -r f2d44c41eedf -r 47b53ae385d1 src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java --- /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 verifiers = new ArrayList(); + // we only do HS256 yet + verifiers.add(new HmacSHA256Verifier(consumerSecret.getBytes("UTF-8"))); + // verify token signature(should really be static...) + new JsonTokenParser(new SystemClock(), null, (Checker[]) null).verify(token, verifiers); + } 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 xpointers = new ArrayList(); + 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 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 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(); + + } + +} diff -r f2d44c41eedf -r 47b53ae385d1 src/main/java/de/mpiwg/itgroup/annotations/restlet/RestServer.java --- 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 results = dctx.search(base, filter, - sc); + NamingEnumeration 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; + } + } diff -r f2d44c41eedf -r 47b53ae385d1 src/main/resources/de/itgroup/annotations/annotator-info.html --- 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 @@ - - -

Annotation manager

-

Annotations overview

- - \ No newline at end of file diff -r f2d44c41eedf -r 47b53ae385d1 src/main/resources/de/mpiwg/itgroup/annotations/annotator-info.html --- /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 @@ + + +

Annotation manager

+

Annotations overview

+ + \ No newline at end of file diff -r f2d44c41eedf -r 47b53ae385d1 src/main/webapp/WEB-INF/web.xml --- 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"> - RESTfulJavaWebServices-Restlet + AnnotationManager-Restlet org.restlet.application