8
|
1 /**
|
|
2 * Base class for Annotator resource classes.
|
|
3 */
|
|
4 package de.mpiwg.itgroup.annotationManager.restlet;
|
|
5
|
|
6 import java.io.UnsupportedEncodingException;
|
|
7 import java.net.URLDecoder;
|
11
|
8 import java.net.URLEncoder;
|
10
|
9 import java.security.MessageDigest;
|
|
10 import java.security.NoSuchAlgorithmException;
|
8
|
11 import java.util.ArrayList;
|
|
12 import java.util.List;
|
|
13 import java.util.regex.Matcher;
|
|
14 import java.util.regex.Pattern;
|
|
15
|
10
|
16 import org.apache.log4j.Logger;
|
|
17 import org.joda.time.DateTime;
|
|
18 import org.joda.time.format.DateTimeFormatter;
|
|
19 import org.joda.time.format.ISODateTimeFormat;
|
8
|
20 import org.json.JSONArray;
|
|
21 import org.json.JSONException;
|
|
22 import org.json.JSONObject;
|
|
23 import org.restlet.data.Form;
|
11
|
24 import org.restlet.data.Status;
|
8
|
25 import org.restlet.representation.Representation;
|
|
26 import org.restlet.resource.Options;
|
|
27 import org.restlet.resource.ServerResource;
|
11
|
28 import org.restlet.security.User;
|
8
|
29
|
|
30 import de.mpiwg.itgroup.annotationManager.Constants.NS;
|
|
31 import de.mpiwg.itgroup.annotationManager.RDFHandling.Convert;
|
|
32
|
|
33 /**
|
|
34 * Base class for Annotator resource classes.
|
|
35 *
|
|
36 * @author dwinter, casties
|
|
37 *
|
|
38 */
|
|
39 public abstract class AnnotatorResourceImpl extends ServerResource {
|
|
40
|
10
|
41 protected Logger logger = Logger.getRootLogger();
|
|
42
|
8
|
43 protected String getAllowedMethodsForHeader() {
|
|
44 return "OPTIONS,GET,POST";
|
|
45 }
|
|
46
|
|
47 /**
|
|
48 * Handle options request to allow CORS for AJAX.
|
|
49 *
|
|
50 * @param entity
|
|
51 */
|
|
52 @Options
|
|
53 public void doOptions(Representation entity) {
|
10
|
54 Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
|
8
|
55 if (responseHeaders == null) {
|
|
56 responseHeaders = new Form();
|
10
|
57 getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
|
8
|
58 }
|
10
|
59 responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
|
8
|
60 // echo back Origin and Request-Headers
|
10
|
61 Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
|
8
|
62 String origin = requestHeaders.getFirstValue("Origin", true);
|
|
63 if (origin == null) {
|
|
64 responseHeaders.add("Access-Control-Allow-Origin", "*");
|
|
65 } else {
|
|
66 responseHeaders.add("Access-Control-Allow-Origin", origin);
|
|
67 }
|
10
|
68 String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
|
8
|
69 if (allowHeaders != null) {
|
|
70 responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
|
|
71 }
|
|
72 responseHeaders.add("Access-Control-Allow-Credentials", "true");
|
|
73 responseHeaders.add("Access-Control-Max-Age", "60");
|
|
74 }
|
|
75
|
|
76 /**
|
10
|
77 * returns if authentication information from headers is valid.
|
|
78 *
|
|
79 * @param entity
|
|
80 * @return
|
|
81 */
|
|
82 public boolean isAuthenticated(Representation entity) {
|
|
83 // get authToken
|
|
84 Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
|
|
85 String consumerKey = requestHeaders.getFirstValue("x-annotator-consumer-key", true);
|
|
86 if (consumerKey == null) {
|
|
87 return false;
|
|
88 }
|
|
89 RestServer restServer = (RestServer) getApplication();
|
|
90 String consumerSecret = restServer.getConsumerSecret(consumerKey);
|
|
91 logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
|
|
92 if (consumerSecret == null) {
|
|
93 return false;
|
|
94 }
|
|
95 String userId = requestHeaders.getFirstValue("x-annotator-user-id", true);
|
|
96 String issueTime = requestHeaders.getFirstValue("x-annotator-auth-token-issue-time", true);
|
|
97 if (userId == null || issueTime == null) {
|
|
98 return false;
|
|
99 }
|
|
100 // compute hashed token based on the values we know
|
|
101 // computed_token = hashlib.sha256(consumer.secret + user_id + issue_time).hexdigest()
|
|
102 String computedToken;
|
|
103 try {
|
|
104 MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
105 String computedString = consumerSecret + userId + issueTime;
|
|
106 md.update(computedString.getBytes("UTF-8"));
|
|
107 byte[] dg = md.digest();
|
|
108 StringBuffer sb = new StringBuffer();
|
|
109 for (byte b : dg) {
|
|
110 sb.append(String.format("%02x", b));
|
|
111 }
|
|
112 computedToken = sb.toString();
|
|
113 } catch (NoSuchAlgorithmException e) {
|
|
114 e.printStackTrace();
|
|
115 return false;
|
|
116 } catch (UnsupportedEncodingException e) {
|
|
117 e.printStackTrace();
|
|
118 return false;
|
|
119 }
|
|
120 // compare to the token we got
|
|
121 String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
|
|
122 logger.debug(String.format("got: authToken=%s consumerSecret=%s userId=%s issueTime=%s", authToken, consumerSecret, userId,
|
|
123 issueTime));
|
|
124 if (!computedToken.equals(authToken)) {
|
|
125 return false;
|
|
126 }
|
|
127 // check token lifetime
|
|
128 // validity = iso8601.parse_date(issue_time)
|
|
129 // expiry = validity + datetime.timedelta(seconds=consumer.ttl)
|
|
130 int tokenTtl = 86400;
|
|
131 DateTime tokenValidity = null;
|
|
132 DateTime tokenExpiry = null;
|
|
133 try {
|
|
134 DateTimeFormatter parser = ISODateTimeFormat.dateTime();
|
|
135 tokenValidity = parser.parseDateTime(issueTime);
|
|
136 String tokenTtlString = requestHeaders.getFirstValue("x-annotator-auth-token-ttl", true);
|
|
137 tokenTtl = Integer.parseInt(tokenTtlString);
|
|
138 tokenExpiry = tokenValidity.plusSeconds(tokenTtl);
|
|
139 } catch (NumberFormatException e) {
|
|
140 e.printStackTrace();
|
|
141 }
|
|
142 if (tokenValidity == null || tokenValidity.isAfterNow() || tokenExpiry.isBeforeNow()) {
|
|
143 return false;
|
|
144 }
|
|
145 // must be ok then
|
|
146 return true;
|
|
147 }
|
|
148
|
|
149 /**
|
11
|
150 * creates Annotator-JSON from an Annotation object.
|
8
|
151 *
|
|
152 * @param annot
|
|
153 * @return
|
|
154 */
|
11
|
155 public JSONObject annot2AnnotatorJSON(Convert.Annotation annot) {
|
8
|
156 JSONObject jo = new JSONObject();
|
|
157 try {
|
|
158 jo.put("text", annot.text);
|
|
159 jo.put("uri", annot.url);
|
|
160
|
|
161 JSONObject userObject = new JSONObject();
|
|
162 userObject.put("id", annot.creator);
|
|
163
|
|
164 RestServer restServer = (RestServer) getApplication();
|
|
165
|
|
166 String userID = annot.creator;
|
|
167 if (userID.startsWith(NS.MPIWG_PERSONS)) {
|
10
|
168 userID = userID.replace(NS.MPIWG_PERSONS, ""); // entferne NAMESPACE
|
8
|
169 }
|
|
170 String userName = restServer.getUserNameFromLdap(userID);
|
|
171 userObject.put("name", userName);
|
|
172
|
|
173 jo.put("user", userObject);
|
|
174
|
|
175 List<String> xpointer = new ArrayList<String>();
|
|
176
|
|
177 if (annot.xpointers == null || annot.xpointers.size() == 0)
|
|
178 xpointer.add(annot.xpointer);
|
|
179 else {
|
|
180 for (String xpointerString : annot.xpointers) {
|
|
181 xpointer.add(xpointerString);
|
|
182 }
|
|
183 }
|
|
184 jo.put("ranges", transformToRanges(xpointer));
|
|
185 jo.put("id", annot.annotationUri);
|
|
186 return jo;
|
|
187 } catch (JSONException e) {
|
|
188 // TODO Auto-generated catch block
|
|
189 e.printStackTrace();
|
|
190 return null;
|
|
191 }
|
|
192 }
|
|
193
|
|
194 private JSONArray transformToRanges(List<String> xpointers) {
|
|
195
|
|
196 JSONArray ja = new JSONArray();
|
|
197
|
|
198 Pattern rg = Pattern
|
|
199 .compile("#xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
|
10
|
200 Pattern rg1 = Pattern.compile("#xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");
|
8
|
201
|
|
202 try {
|
|
203 for (String xpointer : xpointers) {
|
|
204 String decoded = URLDecoder.decode(xpointer, "utf-8");
|
|
205 Matcher m = rg.matcher(decoded);
|
|
206
|
|
207 if (m.find()) {
|
|
208 {
|
|
209 JSONObject jo = new JSONObject();
|
|
210 jo.put("start", m.group(1));
|
|
211 jo.put("startOffset", m.group(2));
|
|
212 jo.put("end", m.group(3));
|
|
213 jo.put("endOffset", m.group(4));
|
|
214 ja.put(jo);
|
|
215 }
|
|
216 }
|
|
217 m = rg1.matcher(xpointer);
|
|
218 if (m.find()) {
|
|
219 JSONObject jo = new JSONObject();
|
|
220 jo.put("start", m.group(1));
|
|
221 jo.put("startOffset", m.group(2));
|
|
222
|
|
223 ja.put(jo);
|
|
224 }
|
|
225 }
|
|
226 } catch (JSONException e) {
|
|
227 // TODO Auto-generated catch block
|
|
228 e.printStackTrace();
|
|
229 } catch (UnsupportedEncodingException e) {
|
|
230 // TODO Auto-generated catch block
|
|
231 e.printStackTrace();
|
|
232 }
|
|
233
|
|
234 return ja;
|
|
235 }
|
|
236
|
11
|
237 /**
|
|
238 * creates an Annotation object with data from JSON.
|
|
239 * uses the specification from the annotator project.
|
|
240 *
|
12
|
241 * @see{https://github.com/okfn/annotator/wiki/Annotation-format}
|
11
|
242 *
|
|
243 * The username will be transformed to an URI if not given already
|
|
244 * as URI, if not it will set to the MPIWG namespace defined in
|
|
245 * de.mpiwg.itgroup.annotationManager.Constants.NS
|
|
246 * @param jo
|
|
247 * @return
|
|
248 * @throws JSONException
|
|
249 */
|
|
250 public Convert.Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException {
|
|
251 Convert.Annotation annot;
|
|
252 String url = jo.getString("uri");
|
|
253 String text = jo.getString("text");
|
|
254
|
|
255 String username = null;
|
|
256 if (jo.has("user")) {
|
|
257 // not required, if no username given authuser
|
|
258 // will be used otherwise username and password
|
|
259 // has to be submitted
|
|
260 JSONObject user = jo.getJSONObject("user");
|
|
261 if (user.has("id")) {
|
|
262 username = user.getString("id");
|
|
263 if (!user.has("password")) {
|
|
264 User authUser = handleBasicAuthentification(entity);
|
|
265 if (authUser == null) {
|
|
266 setStatus(Status.CLIENT_ERROR_FORBIDDEN);
|
|
267 return null;
|
|
268 }
|
|
269 username = authUser.getIdentifier();
|
|
270 } else {
|
|
271 String password = user.getString("password");
|
|
272 if (!((RestServer) getApplication()).authenticate(username, password, getRequest())) {
|
|
273 setStatus(Status.CLIENT_ERROR_FORBIDDEN);
|
|
274 return null;
|
|
275 }
|
|
276 }
|
|
277 }
|
|
278
|
|
279 } else {
|
|
280 User authUser = handleBasicAuthentification(entity);
|
|
281 if (authUser == null) {
|
|
282 setStatus(Status.CLIENT_ERROR_FORBIDDEN);
|
|
283 return null;
|
|
284 }
|
|
285 username = authUser.getIdentifier();
|
|
286 }
|
|
287
|
|
288 String xpointer;
|
|
289 if (jo.has("ranges")) {
|
|
290 JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
|
|
291 String start = ranges.getString("start");
|
|
292 String end = ranges.getString("end");
|
|
293 String startOffset = ranges.getString("startOffset");
|
|
294 String endOffset = ranges.getString("endOffset");
|
|
295
|
|
296 try {
|
|
297 xpointer = url
|
|
298 + "#"
|
|
299 + URLEncoder.encode(String.format(
|
|
300 "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))",
|
|
301 start, startOffset, end, endOffset), "utf-8");
|
|
302 } catch (UnsupportedEncodingException e) {
|
|
303 e.printStackTrace();
|
|
304 setStatus(Status.SERVER_ERROR_INTERNAL);
|
|
305 return null;
|
|
306 }
|
|
307 } else {
|
|
308 xpointer = url;
|
|
309 }
|
|
310
|
|
311 // username should be a URI, if not it will set to the MPIWG namespace defined in
|
|
312 // de.mpiwg.itgroup.annotationManager.Constants.NS
|
|
313 if (!username.startsWith("http"))
|
|
314 username = NS.MPIWG_PERSONS + username;
|
|
315
|
|
316 return new Convert.Annotation(xpointer, username, null, text, null);
|
|
317 }
|
|
318
|
8
|
319 }
|