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