Mercurial > hg > AnnotationManagerN4J
annotate src/main/java/de/mpiwg/itgroup/annotations/restlet/BaseRestlet.java @ 70:2b1e6df5e21a
added lgpl_v3 license information.
author | casties |
---|---|
date | Thu, 06 Mar 2014 15:09:04 +0100 |
parents | 5b568de5ee0d |
children | 326369d4bc4d |
rev | line source |
---|---|
18 | 1 package de.mpiwg.itgroup.annotations.restlet; |
2 | |
70 | 3 /* |
4 * #%L | |
5 * AnnotationManager | |
6 * %% | |
7 * Copyright (C) 2012 - 2014 MPIWG Berlin | |
8 * %% | |
9 * This program is free software: you can redistribute it and/or modify | |
10 * it under the terms of the GNU Lesser General Public License as | |
11 * published by the Free Software Foundation, either version 3 of the | |
12 * License, or (at your option) any later version. | |
13 * | |
14 * This program is distributed in the hope that it will be useful, | |
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
17 * GNU General Lesser Public License for more details. | |
18 * | |
19 * You should have received a copy of the GNU General Lesser Public | |
20 * License along with this program. If not, see | |
21 * <http://www.gnu.org/licenses/lgpl-3.0.html>. | |
22 * #L% | |
23 * Author: Robert Casties (casties@mpiwg-berlin.mpg.de) | |
24 */ | |
25 | |
18 | 26 import java.io.File; |
27 import java.io.FileInputStream; | |
28 import java.io.FileNotFoundException; | |
29 import java.io.IOException; | |
30 import java.io.InputStream; | |
31 import java.util.Hashtable; | |
32 import java.util.Properties; | |
33 | |
34 import javax.naming.NamingEnumeration; | |
35 import javax.naming.NamingException; | |
36 import javax.naming.directory.Attribute; | |
37 import javax.naming.directory.DirContext; | |
38 import javax.naming.directory.InitialDirContext; | |
39 import javax.naming.directory.SearchControls; | |
40 import javax.naming.directory.SearchResult; | |
41 import javax.servlet.ServletContext; | |
42 | |
43 import org.apache.log4j.BasicConfigurator; | |
44 import org.apache.log4j.Logger; | |
45 import org.neo4j.graphdb.GraphDatabaseService; | |
66
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
46 import org.neo4j.graphdb.factory.GraphDatabaseBuilder; |
18 | 47 import org.neo4j.graphdb.factory.GraphDatabaseFactory; |
66
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
48 import org.neo4j.graphdb.factory.GraphDatabaseSettings; |
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
49 import org.neo4j.kernel.GraphDatabaseAPI; |
18 | 50 import org.neo4j.server.WrappingNeoServerBootstrapper; |
51 import org.restlet.Application; | |
52 import org.restlet.Context; | |
53 | |
54 import de.mpiwg.itgroup.annotations.neo4j.AnnotationStore; | |
55 | |
56 public abstract class BaseRestlet extends Application { | |
57 | |
58 public static Logger logger = Logger.getLogger(BaseRestlet.class); | |
59 | |
60 /** | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
61 * Properties holding consumer keys and secrets. |
18 | 62 */ |
63 protected Properties consumerKeys; | |
64 public String CONSUMER_KEYS_PATH = "WEB-INF/consumerkeys.property"; | |
65 public static final String CONSUMERKEYS_KEY = "annotationmanager.consumerkeys"; | |
66 | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
67 /** |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
68 * Properties holding server config. |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
69 */ |
18 | 70 protected Properties serverConfig; |
71 public String CONFIG_PROPS_PATH = "WEB-INF/serverconfig.property"; | |
72 public static final String SERVERCONFIG_KEY = "annotationmanager.serverconfig"; | |
73 | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
74 /** |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
75 * database instance; |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
76 */ |
18 | 77 protected GraphDatabaseService graphDb; |
78 public static final String GRAPHDB_KEY = "annotationmanager.graphdb"; | |
79 public static final String GRAPHDB_PATH_KEY = "annotationmanager.graphdb.path"; | |
80 public String graphdbPath = "WEB-INF/neo4j-annotation-db"; | |
81 | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
82 /** |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
83 * database interface server instance. |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
84 */ |
18 | 85 protected WrappingNeoServerBootstrapper srv; |
86 public static final String GRAPHDBSRV_KEY = "annotationmanager.graphdb.srv"; | |
87 | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
88 /** |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
89 * annotation store instance. |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
90 */ |
18 | 91 protected AnnotationStore store; |
92 public static final String ANNSTORE_KEY = "annotationmanager.store"; | |
93 | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
94 /** |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
95 * LDAP server URI (for looking up full user names). |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
96 */ |
18 | 97 protected String ldapServerUrl; |
98 public static final String LDAP_SERVER_KEY = "annotationmanager.ldapserver.url"; | |
99 | |
50 | 100 public static final String ADMIN_USER_KEY = "annotationmanager.admin.user"; |
101 public static final String ADMIN_PASSWORD_KEY = "annotationmanager.admin.password"; | |
102 | |
18 | 103 /** |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
104 * run in authorization mode i.e. with tokens. |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
105 */ |
58 | 106 protected boolean authorizationMode = false; |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
107 public static final String AUTHORIZATION_MODE_KEY = "annotationmanager.authorization"; |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
108 |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
109 /** |
58 | 110 * prefix to create uris for tags in store. |
111 */ | |
112 public static String TAGS_URI_PREFIX = ""; | |
113 public static final String TAGS_URI_KEY = "annotationmanager.uris.tags"; | |
114 | |
115 /** | |
116 * prefix to create uris for persons in store. | |
117 */ | |
118 public static String PERSONS_URI_PREFIX = ""; | |
119 public static final String PERSONS_URI_KEY = "annotationmanager.uris.persons"; | |
120 | |
121 /** | |
122 * prefix to create uris for groups in store. | |
123 */ | |
124 public static String GROUPS_URI_PREFIX = ""; | |
125 public static final String GROUPS_URI_KEY = "annotationmanager.uris.groups"; | |
126 | |
59
e2f86ef9b871
make annotation uri in store configurable. fix npe with no tags.
casties
parents:
58
diff
changeset
|
127 public static final String ANNOTATIONS_URI_KEY = "annotationmanager.uris.annotations"; |
e2f86ef9b871
make annotation uri in store configurable. fix npe with no tags.
casties
parents:
58
diff
changeset
|
128 |
58 | 129 /** |
18 | 130 * constructor |
131 * | |
132 * @param context | |
133 */ | |
134 public BaseRestlet(Context context) { | |
135 super(context); | |
136 configure(); | |
137 } | |
138 | |
139 /** | |
140 * Configures the restlet. | |
141 * Reads serverConfig, consumerKeys and graphDb config from config files and starts graphDb. | |
142 * Uses config from webapp context if already initialized. | |
143 */ | |
144 protected void configure() { | |
145 ServletContext sc = (ServletContext) getContext().getServerDispatcher().getContext().getAttributes() | |
146 .get("org.restlet.ext.servlet.ServletContext"); | |
147 if (sc != null) { | |
148 if (sc.getAttribute("annotationserver.log4j.configured") == null) { | |
149 // TODO: is this the right place to run the log4j configurator? | |
150 BasicConfigurator.configure(); | |
151 sc.setAttribute("annotationserver.log4j.configured", "done"); | |
152 } | |
153 logger.info(getVersion() + " starting..."); | |
154 | |
155 /* | |
156 * read config from webapp | |
157 */ | |
158 serverConfig = (Properties) sc.getAttribute(SERVERCONFIG_KEY); | |
159 if (serverConfig == null) { | |
160 serverConfig = new Properties(); | |
161 InputStream ps = getResourceAsStream(sc, CONFIG_PROPS_PATH); | |
162 if (ps != null) { | |
163 logger.debug("loading config from " + CONFIG_PROPS_PATH); | |
164 try { | |
165 serverConfig.load(ps); | |
166 /* | |
167 * read serverconfig options | |
168 */ | |
169 graphdbPath = serverConfig.getProperty(GRAPHDB_PATH_KEY, graphdbPath); | |
170 ldapServerUrl = serverConfig.getProperty(LDAP_SERVER_KEY, null); | |
60
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
171 /* |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
172 * uri prefixes |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
173 */ |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
174 if (serverConfig.containsKey(PERSONS_URI_KEY)) { |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
175 BaseRestlet.PERSONS_URI_PREFIX = serverConfig.getProperty(PERSONS_URI_KEY); |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
176 } |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
177 if (serverConfig.containsKey(GROUPS_URI_KEY)) { |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
178 BaseRestlet.GROUPS_URI_PREFIX = serverConfig.getProperty(GROUPS_URI_KEY); |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
179 } |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
180 if (serverConfig.containsKey(TAGS_URI_KEY)) { |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
181 BaseRestlet.TAGS_URI_PREFIX = serverConfig.getProperty(TAGS_URI_KEY); |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
182 } |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
183 if (serverConfig.containsKey(ANNOTATIONS_URI_KEY)) { |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
184 AnnotationStore.ANNOTATION_URI_PREFIX = serverConfig.getProperty(ANNOTATIONS_URI_KEY); |
99d9afcfd04d
configuration of uri prefixes was completely broken.
casties
parents:
59
diff
changeset
|
185 } |
18 | 186 } catch (IOException e) { |
187 logger.warn("Error loading server config: ", e); | |
188 } | |
189 logger.debug("config: " + serverConfig); | |
190 } else { | |
191 logger.error("Unable to get resource " + CONFIG_PROPS_PATH); | |
192 } | |
193 // store config | |
194 sc.setAttribute(SERVERCONFIG_KEY, serverConfig); | |
195 } | |
196 // look for database service in context | |
197 graphDb = (GraphDatabaseService) sc.getAttribute(GRAPHDB_KEY); | |
198 if (graphDb == null) { | |
199 /* | |
200 * open database | |
201 */ | |
202 String dbFn = getResourcePath(sc, graphdbPath); | |
203 if (dbFn != null) { | |
204 logger.debug("opening DB " + dbFn); | |
66
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
205 GraphDatabaseBuilder graphDbBuilder = new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(dbFn); |
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
206 graphDbBuilder.setConfig(GraphDatabaseSettings.allow_store_upgrade, "true"); |
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
207 graphDb = graphDbBuilder.newGraphDatabase(); |
18 | 208 registerShutdownHook(graphDb); |
209 // store in context | |
210 sc.setAttribute(GRAPHDB_KEY, graphDb); | |
211 // AnnotationStore | |
212 store = new AnnotationStore(graphDb); | |
213 sc.setAttribute(ANNSTORE_KEY, store); | |
214 // admin server | |
66
5b568de5ee0d
updated to new Neo4J version 2.0. doesn't use new features. problems with neo4j admin web ui.
casties
parents:
60
diff
changeset
|
215 srv = new WrappingNeoServerBootstrapper((GraphDatabaseAPI) graphDb); |
18 | 216 logger.debug("Starting DB admin server..."); |
217 // store in context | |
218 sc.setAttribute(GRAPHDBSRV_KEY, srv); | |
219 srv.start(); | |
220 } else { | |
221 logger.error("Unable to get resource " + dbFn); | |
222 } | |
22 | 223 } else { |
224 // get existing AnnotationStore | |
225 store = (AnnotationStore) sc.getAttribute(ANNSTORE_KEY); | |
18 | 226 } |
227 /* | |
228 * read consumerKeys from webapp | |
229 */ | |
230 consumerKeys = (Properties) sc.getAttribute(CONSUMERKEYS_KEY); | |
231 if (consumerKeys == null) { | |
232 consumerKeys = new Properties(); | |
233 InputStream ps = getResourceAsStream(sc, CONSUMER_KEYS_PATH); | |
234 if (ps != null) { | |
235 logger.debug("loading consumer keys from " + CONSUMER_KEYS_PATH); | |
236 try { | |
237 consumerKeys.load(ps); | |
238 } catch (IOException e) { | |
239 // TODO Auto-generated catch block | |
240 e.printStackTrace(); | |
241 } | |
242 logger.debug("consumer keys: " + consumerKeys); | |
243 } else { | |
244 logger.error("Unable to get resource " + CONSUMER_KEYS_PATH); | |
245 } | |
246 // store config | |
247 sc.setAttribute(CONSUMERKEYS_KEY, consumerKeys); | |
248 } | |
249 } else { | |
250 logger.error("Unable to get ServletContext!"); | |
251 } | |
252 } | |
253 | |
254 public abstract String getVersion(); | |
255 | |
256 /** | |
57
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
257 * @return the authorizationMode |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
258 */ |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
259 public boolean isAuthorizationMode() { |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
260 return authorizationMode; |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
261 } |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
262 |
4efb21cf0ce0
new non-authorized mode without tokens. enabled by default. configured with annotationmanager.authorization=false property.
casties
parents:
50
diff
changeset
|
263 /** |
19 | 264 * @return the store |
265 */ | |
266 public AnnotationStore getAnnotationStore() { | |
267 return store; | |
268 } | |
269 | |
270 /** | |
18 | 271 * returns consumer secret for consumer key. returns null if consumer key |
272 * doesn't exist. | |
273 * | |
274 * @param consumerKey | |
275 * @return | |
276 */ | |
277 public String getConsumerSecret(String consumerKey) { | |
278 return consumerKeys.getProperty(consumerKey); | |
279 } | |
280 | |
281 /** | |
282 * Hole den vollen Benutzernamen aus dem LDAP | |
283 * | |
284 * @param creator | |
285 * @return | |
286 */ | |
287 public String getFullNameFromLdap(String creator) { | |
288 String retString = creator; // falls nichts gefunden wird einfach den | |
289 // creator zurueckgeben | |
290 if (ldapServerUrl == null) { | |
291 return retString; | |
292 } | |
293 Hashtable<String, String> env = new Hashtable<String, String>(); | |
294 String sp = "com.sun.jndi.ldap.LdapCtxFactory"; | |
295 env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, sp); | |
296 env.put(javax.naming.Context.PROVIDER_URL, ldapServerUrl); | |
297 | |
298 DirContext dctx; | |
299 try { | |
300 dctx = new InitialDirContext(env); | |
301 } catch (NamingException e) { | |
302 logger.error("Error in getFullNameFromLDAP!", e); | |
303 return retString; | |
304 } | |
305 | |
306 String base = "ou=people"; | |
307 | |
308 SearchControls sc = new SearchControls(); | |
309 String[] attributeFilter = { "cn", "mail" }; | |
310 sc.setReturningAttributes(attributeFilter); | |
311 sc.setSearchScope(SearchControls.SUBTREE_SCOPE); | |
312 | |
313 String filter = "(uid=" + creator + ")"; | |
314 | |
315 try { | |
316 NamingEnumeration<SearchResult> results = dctx.search(base, filter, sc); | |
317 while (results.hasMore()) { | |
318 SearchResult sr = (SearchResult) results.next(); | |
319 javax.naming.directory.Attributes attrs = sr.getAttributes(); | |
320 | |
321 Attribute attr = attrs.get("cn"); | |
322 retString = (String) attr.get(); | |
323 } | |
324 } catch (NamingException e) { | |
325 logger.error("Error in getFullNameFromLDAP!", e); | |
326 } | |
327 | |
328 try { | |
329 dctx.close(); | |
330 } catch (NamingException e) { | |
331 logger.error("Error in getFullNameFromLDAP!", e); | |
332 } | |
333 return retString; | |
334 } | |
335 | |
336 /** | |
337 * returns resource from path (in webapp) as InputStream. | |
338 * | |
339 * @param sc | |
340 * @param path | |
341 * @return | |
342 */ | |
343 protected InputStream getResourceAsStream(ServletContext sc, String path) { | |
344 InputStream ps = sc.getResourceAsStream(path); | |
345 if (ps == null) { | |
346 // try as file | |
347 File pf = new File(sc.getRealPath(path)); | |
348 if (pf != null) { | |
349 logger.debug("trying file for: " + pf); | |
350 try { | |
351 ps = new FileInputStream(pf); | |
352 } catch (FileNotFoundException e) { | |
353 logger.error(e); | |
354 } | |
355 } | |
356 } | |
357 return ps; | |
358 } | |
359 | |
360 /** | |
361 * get a real file name for a web app file pathname. | |
362 * | |
363 * If filename starts with "/" its treated as absolute else the path is | |
364 * appended to the base directory of the web-app. | |
365 * | |
366 * @param filename | |
367 * @param sc | |
368 * @return | |
369 */ | |
370 public static String getResourcePath(ServletContext sc, String filename) { | |
371 File f = new File(filename); | |
372 // is the filename absolute? | |
373 if (!f.isAbsolute()) { | |
374 // relative path -> use getRealPath to resolve in webapp | |
375 filename = sc.getRealPath(filename); | |
376 } | |
377 return filename; | |
378 } | |
379 | |
380 /* | |
381 * (non-Javadoc) | |
382 * | |
383 * @see org.restlet.Application#stop() | |
384 */ | |
385 @Override | |
386 public synchronized void stop() throws Exception { | |
387 /* | |
388 * trying to clean up databases, not sure if this is the right way... | |
389 */ | |
390 if (srv != null) { | |
391 logger.debug("Stopping DB admin server..."); | |
392 srv.stop(); | |
393 srv = null; | |
394 } | |
395 if (graphDb != null) { | |
396 logger.debug("Stopping DB..."); | |
397 graphDb.shutdown(); | |
398 graphDb = null; | |
399 } | |
400 super.stop(); | |
401 } | |
402 | |
403 private static void registerShutdownHook(final GraphDatabaseService graphDb) { | |
404 // Registers a shutdown hook for the Neo4j instance so that it | |
405 // shuts down nicely when the VM exits (even if you "Ctrl-C" the | |
406 // running example before it's completed) | |
407 Runtime.getRuntime().addShutdownHook(new Thread() { | |
408 @Override | |
409 public void run() { | |
410 graphDb.shutdown(); | |
411 } | |
412 }); | |
413 } | |
414 | |
415 } |