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