annotate src/de/mpiwg/itgroup/annotationManager/restlet/AnnotatorResourceImpl.java @ 18:383c0fee2d37

PUT returns annotation now.
author casties
date Fri, 23 Mar 2012 12:13:48 +0100
parents b0ef5c860464
children 6629e8422760
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
1 /**
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
2 * Base class for Annotator resource classes.
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
3 */
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
4 package de.mpiwg.itgroup.annotationManager.restlet;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
5
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
6 import java.io.UnsupportedEncodingException;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
7 import java.net.URLDecoder;
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
8 import java.net.URLEncoder;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
9 import java.security.MessageDigest;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
10 import java.security.NoSuchAlgorithmException;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
11 import java.util.ArrayList;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
12 import java.util.List;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
13 import java.util.regex.Matcher;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
14 import java.util.regex.Pattern;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
15
14
0f64de5fff5a try to use javax.xml.bind.DatatypeConverter
casties
parents: 13
diff changeset
16 import javax.xml.bind.DatatypeConverter;
0f64de5fff5a try to use javax.xml.bind.DatatypeConverter
casties
parents: 13
diff changeset
17
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
18 import org.apache.log4j.Logger;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
19 import org.joda.time.DateTime;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
20 import org.joda.time.format.DateTimeFormatter;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
21 import org.joda.time.format.ISODateTimeFormat;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
22 import org.json.JSONArray;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
23 import org.json.JSONException;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
24 import org.json.JSONObject;
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
25 import org.restlet.data.ClientInfo;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
26 import org.restlet.data.Form;
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
27 import org.restlet.data.Status;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
28 import org.restlet.representation.Representation;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
29 import org.restlet.resource.Options;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
30 import org.restlet.resource.ServerResource;
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
31 import org.restlet.security.User;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
32
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
33 import de.mpiwg.itgroup.annotationManager.Constants.NS;
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
34 import de.mpiwg.itgroup.annotationManager.RDFHandling.Annotation;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
35
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
36 /**
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
37 * Base class for Annotator resource classes.
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
38 *
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
39 * @author dwinter, casties
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
40 *
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
41 */
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
42 public abstract class AnnotatorResourceImpl extends ServerResource {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
43
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
44 protected Logger logger = Logger.getRootLogger();
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
45
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
46 protected String getAllowedMethodsForHeader() {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
47 return "OPTIONS,GET,POST";
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
48 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
49
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
50 /**
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
51 * returns a hex String of a SHA256 digest of text.
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
52 *
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
53 * @param text
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
54 * @return
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
55 */
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
56 public String getSha256Digest(String text) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
57 String digest = null;
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
58 try {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
59 MessageDigest md = MessageDigest.getInstance("SHA-256");
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
60 md.update(text.getBytes("UTF-8"));
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
61 byte[] dg = md.digest();
14
0f64de5fff5a try to use javax.xml.bind.DatatypeConverter
casties
parents: 13
diff changeset
62 digest = DatatypeConverter.printHexBinary(dg);
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
63 } catch (NoSuchAlgorithmException e) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
64 e.printStackTrace();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
65 } catch (UnsupportedEncodingException e) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
66 e.printStackTrace();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
67 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
68 return digest;
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
69 }
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
70
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
71 public String encodeJsonId(String id) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
72 try {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
73 return DatatypeConverter.printBase64Binary(id.getBytes("UTF-8"));
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
74 } catch (UnsupportedEncodingException e) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
75 return null;
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
76 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
77 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
78
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
79 public String decodeJsonId(String id) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
80 try {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
81 return new String(DatatypeConverter.parseBase64Binary(id), "UTF-8");
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
82 } catch (UnsupportedEncodingException e) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
83 return null;
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
84 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
85 }
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
86
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
87 /**
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
88 * Handle options request to allow CORS for AJAX.
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
89 *
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
90 * @param entity
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
91 */
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
92 @Options
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
93 public void doOptions(Representation entity) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
94 logger.debug("AnnotatorResourceImpl doOptions!");
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
95 setCorsHeaders();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
96 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
97
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
98 /**
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
99 * set headers to allow CORS for AJAX.
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
100 */
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
101 protected void setCorsHeaders() {
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
102 Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
103 if (responseHeaders == null) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
104 responseHeaders = new Form();
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
105 getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
106 }
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
107 responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
108 // echo back Origin and Request-Headers
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
109 Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
110 String origin = requestHeaders.getFirstValue("Origin", true);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
111 if (origin == null) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
112 responseHeaders.add("Access-Control-Allow-Origin", "*");
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
113 } else {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
114 responseHeaders.add("Access-Control-Allow-Origin", origin);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
115 }
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
116 String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
117 if (allowHeaders != null) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
118 responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
119 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
120 responseHeaders.add("Access-Control-Allow-Credentials", "true");
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
121 responseHeaders.add("Access-Control-Max-Age", "60");
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
122 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
123
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
124 /**
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
125 * returns if authentication information from headers is valid.
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
126 *
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
127 * @param entity
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
128 * @return
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
129 */
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
130 public boolean isAuthenticated(Representation entity) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
131 return (checkAuthToken(entity) != null);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
132 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
133
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
134 /**
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
135 * checks Annotator Auth plugin authentication information from headers. returns userId if successful.
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
136 *
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
137 * @param entity
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
138 * @return
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
139 */
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
140 public String checkAuthToken(Representation entity) {
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
141 Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
142 String consumerKey = requestHeaders.getFirstValue("x-annotator-consumer-key", true);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
143 if (consumerKey == null) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
144 return null;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
145 }
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
146 // get stored consumer secret for key
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
147 RestServer restServer = (RestServer) getApplication();
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
148 String consumerSecret = restServer.getConsumerSecret(consumerKey);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
149 logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
150 if (consumerSecret == null) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
151 return null;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
152 }
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
153 String userId = requestHeaders.getFirstValue("x-annotator-user-id", true);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
154 String issueTime = requestHeaders.getFirstValue("x-annotator-auth-token-issue-time", true);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
155 if (userId == null || issueTime == null) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
156 return null;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
157 }
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
158 // compute hashed token based on the values we know
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
159 // computed_token = hashlib.sha256(consumer.secret + user_id + issue_time).hexdigest()
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
160 String computedToken = getSha256Digest(consumerSecret + userId + issueTime);
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
161 // compare to the token we got
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
162 String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
15
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
163 logger.debug(String.format("got: authToken=%s consumerSecret=%s userId=%s issueTime=%s computedToken=%s",
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
164 authToken, consumerSecret, userId, issueTime, computedToken));
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
165 if (!computedToken.equalsIgnoreCase(authToken)) {
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
166 logger.warn("authToken differ!");
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
167 return null;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
168 }
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
169 // check token lifetime
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
170 // validity = iso8601.parse_date(issue_time)
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
171 // expiry = validity + datetime.timedelta(seconds=consumer.ttl)
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
172 int tokenTtl = 86400;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
173 DateTime tokenValidity = null;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
174 DateTime tokenExpiry = null;
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
175 try {
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
176 DateTimeFormatter parser = ISODateTimeFormat.dateTime();
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
177 tokenValidity = parser.parseDateTime(issueTime);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
178 String tokenTtlString = requestHeaders.getFirstValue("x-annotator-auth-token-ttl", true);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
179 tokenTtl = Integer.parseInt(tokenTtlString);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
180 tokenExpiry = tokenValidity.plusSeconds(tokenTtl);
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
181 } catch (NumberFormatException e) {
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
182 e.printStackTrace();
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
183 }
15
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
184 if (tokenValidity == null || tokenValidity.isAfterNow() || tokenExpiry == null || tokenExpiry.isBeforeNow()) {
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
185 logger.warn(String.format("authToken invalid! tokenValidity=%s tokenExpiry=%s now=%s", tokenValidity, tokenExpiry, DateTime.now()));
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
186 // we dont care about validity right now
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
187 //return null;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
188 }
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
189 // must be ok then
15
6c7c4140630d on the way to updating annotations.
casties
parents: 14
diff changeset
190 logger.debug("auth OK! user="+userId);
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
191 return userId;
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
192 }
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
193
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
194 /**
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
195 * creates Annotator-JSON from an Annotation object.
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
196 *
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
197 * @param annot
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
198 * @return
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
199 */
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
200 public JSONObject createAnnotatorJson(Annotation annot) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
201 boolean makeUserObject = true;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
202 JSONObject jo = new JSONObject();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
203 try {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
204 jo.put("text", annot.text);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
205 jo.put("uri", annot.url);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
206
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
207 if (makeUserObject) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
208 // create user object
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
209 JSONObject userObject = new JSONObject();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
210 // save creator as uri
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
211 userObject.put("uri", annot.creator);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
212 // make short user id
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
213 String userID = annot.creator;
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
214 if (userID.startsWith(NS.MPIWG_PERSONS)) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
215 userID = userID.replace(NS.MPIWG_PERSONS, ""); // entferne NAMESPACE
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
216 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
217 // save as id
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
218 userObject.put("id", userID);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
219 // get full name
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
220 RestServer restServer = (RestServer) getApplication();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
221 String userName = restServer.getUserNameFromLdap(userID);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
222 userObject.put("name", userName);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
223 // save user object
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
224 jo.put("user", userObject);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
225 } else {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
226 // save user as string
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
227 jo.put("user", annot.creator);
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
228 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
229
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
230 List<String> xpointers = new ArrayList<String>();
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
231 if (annot.xpointers == null || annot.xpointers.size() == 0)
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
232 xpointers.add(annot.xpointer);
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
233 else {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
234 for (String xpointerString : annot.xpointers) {
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
235 xpointers.add(xpointerString);
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
236 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
237 }
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
238 jo.put("ranges", transformToRanges(xpointers));
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
239 // encode Annotation URL (=id) in base64
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
240 String annotUrl = annot.getAnnotationUri();
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
241 String annotId = encodeJsonId(annotUrl);
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
242 jo.put("id", annotId);
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
243 return jo;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
244 } catch (JSONException e) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
245 // TODO Auto-generated catch block
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
246 e.printStackTrace();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
247 }
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
248 return null;
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
249 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
250
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
251 private JSONArray transformToRanges(List<String> xpointers) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
252
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
253 JSONArray ja = new JSONArray();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
254
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
255 Pattern rg = Pattern
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
256 .compile("#xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
10
0bdfe01e30b5 checking auth token works now.
casties
parents: 8
diff changeset
257 Pattern rg1 = Pattern.compile("#xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
258
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
259 try {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
260 for (String xpointer : xpointers) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
261 String decoded = URLDecoder.decode(xpointer, "utf-8");
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
262 Matcher m = rg.matcher(decoded);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
263
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
264 if (m.find()) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
265 {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
266 JSONObject jo = new JSONObject();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
267 jo.put("start", m.group(1));
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
268 jo.put("startOffset", m.group(2));
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
269 jo.put("end", m.group(3));
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
270 jo.put("endOffset", m.group(4));
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
271 ja.put(jo);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
272 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
273 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
274 m = rg1.matcher(xpointer);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
275 if (m.find()) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
276 JSONObject jo = new JSONObject();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
277 jo.put("start", m.group(1));
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
278 jo.put("startOffset", m.group(2));
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
279
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
280 ja.put(jo);
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
281 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
282 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
283 } catch (JSONException e) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
284 // TODO Auto-generated catch block
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
285 e.printStackTrace();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
286 } catch (UnsupportedEncodingException e) {
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
287 // TODO Auto-generated catch block
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
288 e.printStackTrace();
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
289 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
290
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
291 return ja;
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
292 }
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
293
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
294 /**
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
295 * creates an Annotation object with data from JSON.
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
296 *
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
297 * uses the specification from the annotator project: {@link https://github.com/okfn/annotator/wiki/Annotation-format}
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
298 *
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
299 * 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
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
300 * de.mpiwg.itgroup.annotationManager.Constants.NS
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
301 *
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
302 * @param jo
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
303 * @return
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
304 * @throws JSONException
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
305 */
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
306 public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
307 return updateAnnotation(new Annotation(), jo, entity);
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
308 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
309
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
310 public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
311 // annotated uri
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
312 String url = annot.url;
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
313 if (jo.has("uri")) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
314 url = jo.getString("uri");
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
315 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
316 // annotation text
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
317 String text = annot.text;
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
318 if (jo.has("text")) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
319 text = jo.getString("text");
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
320 }
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
321 // check authentication
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
322 String authUser = checkAuthToken(entity);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
323 if (authUser == null) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
324 // try http auth
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
325 User httpUser = getHttpAuthUser(entity);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
326 if (httpUser == null) {
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
327 setStatus(Status.CLIENT_ERROR_FORBIDDEN);
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
328 return null;
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
329 }
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
330 authUser = httpUser.getIdentifier();
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
331 }
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
332 // username not required, if no username given authuser will be used
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
333 String username = null;
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
334 String userUri = annot.creator;
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
335 if (jo.has("user")) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
336 if (jo.get("user") instanceof String) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
337 // user is just a String
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
338 username = jo.getString("user");
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
339 // TODO: what if username and authUser are different?
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
340 } else {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
341 // user is an object
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
342 JSONObject user = jo.getJSONObject("user");
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
343 if (user.has("id")) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
344 username = user.getString("id");
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
345 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
346 if (user.has("uri")) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
347 userUri = user.getString("uri");
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
348 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
349 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
350 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
351 if (username == null) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
352 username = authUser;
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
353 }
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
354 // username should be a URI, if not it will set to the MPIWG namespace defined in
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
355 // de.mpiwg.itgroup.annotationManager.Constants.NS
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
356 if (userUri == null) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
357 if (username.startsWith("http")) {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
358 userUri = username;
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
359 } else {
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
360 userUri = NS.MPIWG_PERSONS + username;
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
361 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
362 }
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
363 // TODO: should we overwrite the creator?
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
364
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
365 // create xpointer
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
366 String xpointer = annot.xpointer;
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
367 if (jo.has("ranges")) {
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
368 JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
369 String start = ranges.getString("start");
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
370 String end = ranges.getString("end");
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
371 String startOffset = ranges.getString("startOffset");
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
372 String endOffset = ranges.getString("endOffset");
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
373
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
374 try {
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
375 xpointer = url
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
376 + "#"
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
377 + URLEncoder.encode(String.format(
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
378 "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))",
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
379 start, startOffset, end, endOffset), "utf-8");
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
380 } catch (UnsupportedEncodingException e) {
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
381 e.printStackTrace();
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
382 setStatus(Status.SERVER_ERROR_INTERNAL);
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
383 return null;
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
384 }
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
385 }
17
b0ef5c860464 updating and deleting annotations works now!
casties
parents: 15
diff changeset
386 return new Annotation(xpointer, userUri, annot.time, text, annot.type);
13
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
387 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
388
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
389 /**
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
390 * returns the logged in User.
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
391 *
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
392 * @param entity
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
393 * @return
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
394 */
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
395 protected User getHttpAuthUser(Representation entity) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
396 RestServer restServer = (RestServer) getApplication();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
397 if (!restServer.authenticate(getRequest(), getResponse())) {
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
398 // Not authenticated
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
399 return null;
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
400 }
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
401
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
402 ClientInfo ci = getRequest().getClientInfo();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
403 logger.debug(ci);
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
404 return getRequest().getClientInfo().getUser();
9393c9c9b916 saves annotations now!
casties
parents: 12
diff changeset
405
11
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
406 }
2f8c72ae4c43 working on create and read api for annotator.
casties
parents: 10
diff changeset
407
8
11baadcdd2c8 start of new Annotator API implementation.
casties
parents:
diff changeset
408 }