3
|
1 /**
|
|
2 * Base class for Annotator resource classes.
|
|
3 */
|
|
4 package de.mpiwg.itgroup.annotations.restlet;
|
|
5
|
|
6 import java.io.UnsupportedEncodingException;
|
|
7 import java.security.InvalidKeyException;
|
|
8 import java.security.SignatureException;
|
5
|
9 import java.text.SimpleDateFormat;
|
3
|
10 import java.util.ArrayList;
|
5
|
11 import java.util.Calendar;
|
3
|
12 import java.util.List;
|
|
13 import java.util.regex.Matcher;
|
|
14 import java.util.regex.Pattern;
|
|
15
|
4
|
16 import javax.servlet.ServletContext;
|
|
17
|
3
|
18 import net.oauth.jsontoken.Checker;
|
|
19 import net.oauth.jsontoken.JsonToken;
|
|
20 import net.oauth.jsontoken.JsonTokenParser;
|
|
21 import net.oauth.jsontoken.SystemClock;
|
|
22 import net.oauth.jsontoken.crypto.HmacSHA256Verifier;
|
|
23 import net.oauth.jsontoken.crypto.Verifier;
|
|
24
|
|
25 import org.apache.commons.codec.binary.Base64;
|
|
26 import org.apache.log4j.Logger;
|
|
27 import org.json.JSONArray;
|
|
28 import org.json.JSONException;
|
|
29 import org.json.JSONObject;
|
|
30 import org.restlet.data.Form;
|
|
31 import org.restlet.data.Status;
|
|
32 import org.restlet.representation.Representation;
|
|
33 import org.restlet.resource.Options;
|
|
34 import org.restlet.resource.ServerResource;
|
|
35
|
9
|
36 import de.mpiwg.itgroup.annotations.Actor;
|
4
|
37 import de.mpiwg.itgroup.annotations.Annotation;
|
|
38 import de.mpiwg.itgroup.annotations.Annotation.FragmentTypes;
|
10
|
39 import de.mpiwg.itgroup.annotations.Group;
|
|
40 import de.mpiwg.itgroup.annotations.Person;
|
4
|
41 import de.mpiwg.itgroup.annotations.neo4j.AnnotationStore;
|
|
42 import de.mpiwg.itgroup.annotations.old.NS;
|
3
|
43
|
|
44 /**
|
|
45 * Base class for Annotator resource classes.
|
|
46 *
|
|
47 * @author dwinter, casties
|
|
48 *
|
|
49 */
|
|
50 public abstract class AnnotatorResourceImpl extends ServerResource {
|
|
51
|
4
|
52 protected static Logger logger = Logger.getLogger(AnnotatorResourceImpl.class);
|
|
53
|
|
54 private AnnotationStore store;
|
3
|
55
|
|
56 protected String getAllowedMethodsForHeader() {
|
|
57 return "OPTIONS,GET,POST";
|
|
58 }
|
|
59
|
4
|
60 protected AnnotationStore getAnnotationStore() {
|
|
61 if (store == null) {
|
|
62 ServletContext sc = (ServletContext) getContext().getServerDispatcher().getContext().getAttributes()
|
|
63 .get("org.restlet.ext.servlet.ServletContext");
|
|
64 logger.debug("Getting AnnotationStore from Context");
|
|
65 store = (AnnotationStore) sc.getAttribute(RestServer.ANNSTORE_KEY);
|
|
66 }
|
|
67 return store;
|
|
68 }
|
|
69
|
3
|
70 public String encodeJsonId(String id) {
|
|
71 try {
|
|
72 return Base64.encodeBase64URLSafeString(id.getBytes("UTF-8"));
|
|
73 } catch (UnsupportedEncodingException e) {
|
|
74 return null;
|
|
75 }
|
|
76 }
|
|
77
|
|
78 public String decodeJsonId(String id) {
|
|
79 try {
|
|
80 return new String(Base64.decodeBase64(id), "UTF-8");
|
|
81 } catch (UnsupportedEncodingException e) {
|
|
82 return null;
|
|
83 }
|
|
84 }
|
|
85
|
|
86 /**
|
|
87 * Handle options request to allow CORS for AJAX.
|
|
88 *
|
|
89 * @param entity
|
|
90 */
|
|
91 @Options
|
|
92 public void doOptions(Representation entity) {
|
|
93 logger.debug("AnnotatorResourceImpl doOptions!");
|
|
94 setCorsHeaders();
|
|
95 }
|
|
96
|
|
97 /**
|
|
98 * set headers to allow CORS for AJAX.
|
|
99 */
|
|
100 protected void setCorsHeaders() {
|
|
101 Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
|
|
102 if (responseHeaders == null) {
|
|
103 responseHeaders = new Form();
|
|
104 getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
|
|
105 }
|
|
106 responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
|
|
107 // echo back Origin and Request-Headers
|
|
108 Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
|
|
109 String origin = requestHeaders.getFirstValue("Origin", true);
|
|
110 if (origin == null) {
|
|
111 responseHeaders.add("Access-Control-Allow-Origin", "*");
|
|
112 } else {
|
|
113 responseHeaders.add("Access-Control-Allow-Origin", origin);
|
|
114 }
|
|
115 String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
|
|
116 if (allowHeaders != null) {
|
|
117 responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
|
|
118 }
|
|
119 responseHeaders.add("Access-Control-Allow-Credentials", "true");
|
|
120 responseHeaders.add("Access-Control-Max-Age", "60");
|
|
121 }
|
|
122
|
|
123 /**
|
|
124 * returns if authentication information from headers is valid.
|
|
125 *
|
|
126 * @param entity
|
|
127 * @return
|
|
128 */
|
|
129 public boolean isAuthenticated(Representation entity) {
|
|
130 return (checkAuthToken(entity) != null);
|
|
131 }
|
|
132
|
|
133 /**
|
4
|
134 * checks Annotator Auth plugin authentication information from headers.
|
|
135 * returns userId if successful.
|
3
|
136 *
|
|
137 * @param entity
|
|
138 * @return
|
|
139 */
|
|
140 public String checkAuthToken(Representation entity) {
|
|
141 Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
|
|
142 String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
|
|
143 // decode token first to get consumer key
|
|
144 JsonToken token = new JsonTokenParser(null, null).deserialize(authToken);
|
|
145 String userId = token.getParamAsPrimitive("userId").getAsString();
|
|
146 String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString();
|
|
147 // get stored consumer secret for key
|
|
148 RestServer restServer = (RestServer) getApplication();
|
|
149 String consumerSecret = restServer.getConsumerSecret(consumerKey);
|
|
150 logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
|
|
151 if (consumerSecret == null) {
|
|
152 return null;
|
|
153 }
|
|
154 // logger.debug(String.format("token=%s tokenString=%s signatureAlgorithm=%s",token,token.getTokenString(),token.getSignatureAlgorithm()));
|
|
155 try {
|
|
156 List<Verifier> verifiers = new ArrayList<Verifier>();
|
|
157 // we only do HS256 yet
|
|
158 verifiers.add(new HmacSHA256Verifier(consumerSecret.getBytes("UTF-8")));
|
|
159 // verify token signature(should really be static...)
|
|
160 new JsonTokenParser(new SystemClock(), null, (Checker[]) null).verify(token, verifiers);
|
|
161 } catch (SignatureException e) {
|
|
162 // TODO Auto-generated catch block
|
|
163 e.printStackTrace();
|
|
164 } catch (InvalidKeyException e) {
|
|
165 // TODO Auto-generated catch block
|
|
166 e.printStackTrace();
|
|
167 } catch (UnsupportedEncodingException e) {
|
|
168 // TODO Auto-generated catch block
|
|
169 e.printStackTrace();
|
|
170 }
|
|
171 // must be ok then
|
|
172 logger.debug("auth OK! user=" + userId);
|
|
173 return userId;
|
|
174 }
|
|
175
|
|
176 /**
|
|
177 * creates Annotator-JSON from an Annotation object.
|
|
178 *
|
|
179 * @param annot
|
|
180 * @return
|
|
181 */
|
|
182 public JSONObject createAnnotatorJson(Annotation annot) {
|
5
|
183 // return user as a JSON object (otherwise just as string)
|
3
|
184 boolean makeUserObject = true;
|
|
185 JSONObject jo = new JSONObject();
|
|
186 try {
|
4
|
187 jo.put("text", annot.getBodyText());
|
|
188 jo.put("uri", annot.getTargetBaseUri());
|
3
|
189
|
|
190 if (makeUserObject) {
|
|
191 // create user object
|
|
192 JSONObject userObject = new JSONObject();
|
10
|
193 Actor creator = annot.getCreator();
|
3
|
194 // save creator as uri
|
10
|
195 userObject.put("uri", creator.getUri());
|
3
|
196 // make short user id
|
10
|
197 String userId = creator.getIdString();
|
9
|
198 // set as id
|
4
|
199 userObject.put("id", userId);
|
3
|
200 // get full name
|
10
|
201 String userName = creator.getName();
|
5
|
202 if (userName == null) {
|
|
203 RestServer restServer = (RestServer) getApplication();
|
|
204 userName = restServer.getFullNameFromLdap(userId);
|
|
205 }
|
3
|
206 userObject.put("name", userName);
|
|
207 // save user object
|
|
208 jo.put("user", userObject);
|
|
209 } else {
|
|
210 // save user as string
|
4
|
211 jo.put("user", annot.getCreatorUri());
|
3
|
212 }
|
|
213
|
4
|
214 if (annot.getTargetFragment() != null) {
|
3
|
215 // we only look at the first xpointer
|
4
|
216 List<String> fragments = new ArrayList<String>();
|
|
217 fragments.add(annot.getTargetFragment());
|
|
218 FragmentTypes xt = annot.getFragmentType();
|
|
219 if (xt == FragmentTypes.XPOINTER) {
|
|
220 jo.put("ranges", transformToRanges(fragments));
|
|
221 } else if (xt == FragmentTypes.AREA) {
|
|
222 jo.put("areas", transformToAreas(fragments));
|
3
|
223 }
|
|
224 }
|
10
|
225
|
|
226 // permissions
|
|
227 JSONObject perms = new JSONObject();
|
|
228 jo.put("permissions", perms);
|
|
229 // admin
|
|
230 JSONArray adminPerms = new JSONArray();
|
|
231 perms.put("admin", adminPerms);
|
|
232 Actor adminPerm = annot.getAdminPermission();
|
|
233 if (adminPerm != null) {
|
|
234 adminPerms.put(adminPerm.getIdString());
|
|
235 }
|
|
236 // delete
|
|
237 JSONArray deletePerms = new JSONArray();
|
|
238 perms.put("delete", deletePerms);
|
|
239 Actor deletePerm = annot.getDeletePermission();
|
|
240 if (deletePerm != null) {
|
|
241 deletePerms.put(deletePerm.getIdString());
|
|
242 }
|
|
243 // update
|
|
244 JSONArray updatePerms = new JSONArray();
|
|
245 perms.put("update", updatePerms);
|
|
246 Actor updatePerm = annot.getUpdatePermission();
|
|
247 if (updatePerm != null) {
|
|
248 updatePerms.put(updatePerm.getIdString());
|
|
249 }
|
|
250 // read
|
|
251 JSONArray readPerms = new JSONArray();
|
|
252 perms.put("read", readPerms);
|
|
253 Actor readPerm = annot.getReadPermission();
|
|
254 if (readPerm != null) {
|
|
255 readPerms.put(readPerm.getIdString());
|
|
256 }
|
|
257
|
3
|
258 // encode Annotation URL (=id) in base64
|
4
|
259 String annotUrl = annot.getUri();
|
3
|
260 String annotId = encodeJsonId(annotUrl);
|
|
261 jo.put("id", annotId);
|
|
262 return jo;
|
|
263 } catch (JSONException e) {
|
|
264 // TODO Auto-generated catch block
|
|
265 e.printStackTrace();
|
|
266 }
|
|
267 return null;
|
|
268 }
|
|
269
|
|
270 private JSONArray transformToRanges(List<String> xpointers) {
|
|
271
|
|
272 JSONArray ja = new JSONArray();
|
|
273
|
|
274 Pattern rg = Pattern
|
4
|
275 .compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
|
|
276 Pattern rg1 = Pattern.compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");
|
3
|
277
|
|
278 try {
|
|
279 for (String xpointer : xpointers) {
|
10
|
280 // String decoded = URLDecoder.decode(xpointer, "utf-8");
|
5
|
281 String decoded = xpointer;
|
3
|
282 Matcher m = rg.matcher(decoded);
|
|
283
|
|
284 if (m.find()) {
|
|
285 {
|
|
286 JSONObject jo = new JSONObject();
|
|
287 jo.put("start", m.group(1));
|
|
288 jo.put("startOffset", m.group(2));
|
|
289 jo.put("end", m.group(3));
|
|
290 jo.put("endOffset", m.group(4));
|
|
291 ja.put(jo);
|
|
292 }
|
|
293 }
|
|
294 m = rg1.matcher(xpointer);
|
|
295 if (m.find()) {
|
|
296 JSONObject jo = new JSONObject();
|
|
297 jo.put("start", m.group(1));
|
|
298 jo.put("startOffset", m.group(2));
|
|
299
|
|
300 ja.put(jo);
|
|
301 }
|
|
302 }
|
|
303 } catch (JSONException e) {
|
|
304 // TODO Auto-generated catch block
|
|
305 e.printStackTrace();
|
|
306 }
|
|
307 return ja;
|
|
308 }
|
|
309
|
|
310 private JSONArray transformToAreas(List<String> xpointers) {
|
|
311
|
|
312 JSONArray ja = new JSONArray();
|
|
313
|
4
|
314 Pattern rg = Pattern.compile("xywh=(\\w*:)([\\d\\.]+),([\\d\\.]+),([\\d\\.]+),([\\d\\.]+)");
|
3
|
315
|
|
316 try {
|
|
317 for (String xpointer : xpointers) {
|
10
|
318 // String decoded = URLDecoder.decode(xpointer, "utf-8");
|
5
|
319 String decoded = xpointer;
|
3
|
320 Matcher m = rg.matcher(decoded);
|
|
321
|
|
322 if (m.find()) {
|
|
323 {
|
|
324 JSONObject jo = new JSONObject();
|
10
|
325 @SuppressWarnings("unused")
|
3
|
326 String unit = m.group(1);
|
|
327 jo.put("x", m.group(2));
|
|
328 jo.put("y", m.group(3));
|
|
329 jo.put("width", m.group(4));
|
|
330 jo.put("height", m.group(5));
|
|
331 ja.put(jo);
|
|
332 }
|
|
333 }
|
|
334 }
|
|
335 } catch (JSONException e) {
|
|
336 // TODO Auto-generated catch block
|
|
337 e.printStackTrace();
|
|
338 }
|
|
339 return ja;
|
|
340 }
|
|
341
|
5
|
342 protected String parseArea(JSONObject area) throws JSONException {
|
4
|
343 String x = area.getString("x");
|
|
344 String y = area.getString("y");
|
|
345 String width = "0";
|
|
346 String height = "0";
|
|
347 if (area.has("width")) {
|
|
348 width = area.getString("width");
|
|
349 height = area.getString("height");
|
|
350 }
|
5
|
351 String fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height);
|
4
|
352 return fragment;
|
|
353 }
|
|
354
|
5
|
355 protected String parseRange(JSONObject range) throws JSONException {
|
4
|
356 String start = range.getString("start");
|
|
357 String end = range.getString("end");
|
|
358 String startOffset = range.getString("startOffset");
|
|
359 String endOffset = range.getString("endOffset");
|
|
360
|
5
|
361 String fragment = String.format(
|
4
|
362 "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))", start,
|
5
|
363 startOffset, end, endOffset);
|
4
|
364 return fragment;
|
|
365 }
|
|
366
|
3
|
367 /**
|
5
|
368 * Creates an Annotation object with data from JSON.
|
3
|
369 *
|
4
|
370 * uses the specification from the annotator project: {@link https
|
|
371 * ://github.com/okfn/annotator/wiki/Annotation-format}
|
3
|
372 *
|
4
|
373 * The username will be transformed to an URI if not given already as URI,
|
|
374 * if not it will set to the MPIWG namespace defined in
|
3
|
375 * de.mpiwg.itgroup.annotationManager.Constants.NS
|
|
376 *
|
|
377 * @param jo
|
|
378 * @return
|
|
379 * @throws JSONException
|
4
|
380 * @throws UnsupportedEncodingException
|
3
|
381 */
|
4
|
382 public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException {
|
3
|
383 return updateAnnotation(new Annotation(), jo, entity);
|
|
384 }
|
|
385
|
5
|
386 /**
|
|
387 * Updates an Annotation object with data from JSON.
|
|
388 *
|
|
389 * uses the specification from the annotator project: {@link https
|
|
390 * ://github.com/okfn/annotator/wiki/Annotation-format}
|
|
391 *
|
|
392 * The username will be transformed to an URI if not given already as URI,
|
|
393 * if not it will set to the MPIWG namespace defined in
|
|
394 * de.mpiwg.itgroup.annotationManager.Constants.NS
|
|
395 *
|
|
396 * @param annot
|
|
397 * @param jo
|
|
398 * @return
|
|
399 * @throws JSONException
|
|
400 * @throws UnsupportedEncodingException
|
|
401 */
|
4
|
402 public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException,
|
|
403 UnsupportedEncodingException {
|
5
|
404 // target uri
|
3
|
405 if (jo.has("uri")) {
|
4
|
406 annot.setTargetBaseUri(jo.getString("uri"));
|
3
|
407 }
|
|
408 // annotation text
|
|
409 if (jo.has("text")) {
|
4
|
410 annot.setBodyText(jo.getString("text"));
|
3
|
411 }
|
|
412 // check authentication
|
|
413 String authUser = checkAuthToken(entity);
|
|
414 if (authUser == null) {
|
4
|
415 /*
|
|
416 * // try http auth User httpUser = getHttpAuthUser(entity); if
|
|
417 * (httpUser == null) {
|
|
418 */
|
|
419 setStatus(Status.CLIENT_ERROR_FORBIDDEN);
|
|
420 return null;
|
|
421 /*
|
|
422 * } authUser = httpUser.getIdentifier();
|
|
423 */
|
3
|
424 }
|
9
|
425 // get or create creator object
|
|
426 Actor creator = annot.getCreator();
|
|
427 if (creator == null) {
|
10
|
428 creator = new Person();
|
9
|
429 annot.setCreator(creator);
|
|
430 }
|
3
|
431 // username not required, if no username given authuser will be used
|
|
432 String username = null;
|
10
|
433 String userUri = creator.getUri();
|
3
|
434 if (jo.has("user")) {
|
|
435 if (jo.get("user") instanceof String) {
|
|
436 // user is just a String
|
|
437 username = jo.getString("user");
|
10
|
438 creator.setId(username);
|
3
|
439 // TODO: what if username and authUser are different?
|
|
440 } else {
|
|
441 // user is an object
|
|
442 JSONObject user = jo.getJSONObject("user");
|
|
443 if (user.has("id")) {
|
10
|
444 String id = user.getString("id");
|
|
445 creator.setId(id);
|
|
446 username = id;
|
3
|
447 }
|
|
448 if (user.has("uri")) {
|
|
449 userUri = user.getString("uri");
|
|
450 }
|
|
451 }
|
|
452 }
|
|
453 if (username == null) {
|
|
454 username = authUser;
|
|
455 }
|
5
|
456 // try to get full name
|
9
|
457 if (creator.getName() == null && username != null) {
|
5
|
458 RestServer restServer = (RestServer) getApplication();
|
|
459 String fullName = restServer.getFullNameFromLdap(username);
|
9
|
460 creator.setName(fullName);
|
5
|
461 }
|
|
462 // userUri should be a URI, if not it will set to the MPIWG namespace
|
3
|
463 if (userUri == null) {
|
|
464 if (username.startsWith("http")) {
|
|
465 userUri = username;
|
|
466 } else {
|
|
467 userUri = NS.MPIWG_PERSONS_URL + username;
|
|
468 }
|
|
469 }
|
|
470 // TODO: should we overwrite the creator?
|
9
|
471 if (creator.getUri() == null) {
|
|
472 creator.setUri(userUri);
|
5
|
473 }
|
10
|
474
|
5
|
475 if (annot.getCreated() == null) {
|
|
476 // set creation date
|
|
477 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
|
478 String ct = format.format(Calendar.getInstance().getTime());
|
|
479 annot.setCreated(ct);
|
|
480 }
|
3
|
481
|
|
482 // create xpointer from the first range/area
|
|
483 if (jo.has("ranges")) {
|
|
484 JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
|
4
|
485 annot.setFragmentType(FragmentTypes.XPOINTER);
|
|
486 String fragment = parseRange(ranges);
|
|
487 annot.setTargetFragment(fragment);
|
3
|
488 }
|
|
489 if (jo.has("areas")) {
|
|
490 JSONObject area = jo.getJSONArray("areas").getJSONObject(0);
|
4
|
491 annot.setFragmentType(FragmentTypes.AREA);
|
|
492 String fragment = parseArea(area);
|
|
493 annot.setTargetFragment(fragment);
|
3
|
494 }
|
10
|
495
|
|
496 // permissions
|
|
497 if (jo.has("permissions")) {
|
|
498 JSONObject permissions = jo.getJSONObject("permissions");
|
|
499 if (permissions.has("admin")) {
|
|
500 JSONArray perms = permissions.getJSONArray("admin");
|
|
501 Actor actor = getActorFromPermissions(perms);
|
|
502 annot.setAdminPermission(actor);
|
|
503 }
|
|
504 if (permissions.has("delete")) {
|
|
505 JSONArray perms = permissions.getJSONArray("delete");
|
|
506 Actor actor = getActorFromPermissions(perms);
|
|
507 annot.setDeletePermission(actor);
|
|
508 }
|
|
509 if (permissions.has("update")) {
|
|
510 JSONArray perms = permissions.getJSONArray("update");
|
|
511 Actor actor = getActorFromPermissions(perms);
|
|
512 annot.setUpdatePermission(actor);
|
|
513 }
|
|
514 if (permissions.has("read")) {
|
|
515 JSONArray perms = permissions.getJSONArray("read");
|
|
516 Actor actor = getActorFromPermissions(perms);
|
|
517 annot.setReadPermission(actor);
|
|
518 }
|
|
519 }
|
|
520
|
4
|
521 return annot;
|
3
|
522 }
|
|
523
|
10
|
524 @SuppressWarnings("unused")
|
|
525 protected Actor getActorFromPermissions(JSONArray perms) throws JSONException {
|
|
526 Actor actor = null;
|
|
527 for (int i = 0; i < perms.length(); ++i) {
|
|
528 String perm = perms.getString(i);
|
|
529 if (perm.toLowerCase().startsWith("group:")) {
|
|
530 String groupId = perm.substring(6);
|
|
531 actor = new Group(groupId);
|
|
532 } else {
|
|
533 actor = new Person(perm);
|
|
534 }
|
|
535 // we just take the first one
|
|
536 break;
|
|
537 }
|
|
538 return actor;
|
|
539 }
|
|
540
|
3
|
541 }
|