1 | package org.apache.velocity.servlet; |
2 | |
3 | /* |
4 | * Copyright 2000-2001,2004 The Apache Software Foundation. |
5 | * |
6 | * Licensed under the Apache License, Version 2.0 (the "License"); |
7 | * you may not use this file except in compliance with the License. |
8 | * You may obtain a copy of the License at |
9 | * |
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * |
12 | * Unless required by applicable law or agreed to in writing, software |
13 | * distributed under the License is distributed on an "AS IS" BASIS, |
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15 | * See the License for the specific language governing permissions and |
16 | * limitations under the License. |
17 | */ |
18 | |
19 | import java.io.IOException; |
20 | import java.io.PrintWriter; |
21 | import java.io.StringWriter; |
22 | import java.io.OutputStreamWriter; |
23 | import java.io.FileInputStream; |
24 | import java.io.FileNotFoundException; |
25 | import java.io.UnsupportedEncodingException; |
26 | |
27 | import java.util.Properties; |
28 | |
29 | import javax.servlet.ServletConfig; |
30 | import javax.servlet.ServletContext; |
31 | import javax.servlet.ServletException; |
32 | import javax.servlet.ServletOutputStream; |
33 | |
34 | import javax.servlet.http.HttpServlet; |
35 | import javax.servlet.http.HttpServletRequest; |
36 | import javax.servlet.http.HttpServletResponse; |
37 | |
38 | import org.apache.velocity.Template; |
39 | import org.apache.velocity.runtime.RuntimeConstants; |
40 | import org.apache.velocity.runtime.RuntimeSingleton; |
41 | import org.apache.velocity.io.VelocityWriter; |
42 | import org.apache.velocity.util.SimplePool; |
43 | |
44 | import org.apache.velocity.context.Context; |
45 | import org.apache.velocity.VelocityContext; |
46 | |
47 | import org.apache.velocity.app.Velocity; |
48 | |
49 | import org.apache.velocity.exception.ResourceNotFoundException; |
50 | import org.apache.velocity.exception.ParseErrorException; |
51 | import org.apache.velocity.exception.MethodInvocationException; |
52 | |
53 | /** |
54 | * Base class which simplifies the use of Velocity with Servlets. |
55 | * Extend this class, implement the <code>handleRequest()</code> method, |
56 | * and add your data to the context. Then call |
57 | * <code>getTemplate("myTemplate.wm")</code>. |
58 | * |
59 | * This class puts some things into the context object that you should |
60 | * be aware of: |
61 | * <pre> |
62 | * "req" - The HttpServletRequest object |
63 | * "res" - The HttpServletResponse object |
64 | * </pre> |
65 | * |
66 | * There are other methods you can override to access, alter or control |
67 | * any part of the request processing chain. Please see the javadocs for |
68 | * more information on : |
69 | * <ul> |
70 | * <li> loadConfiguration() : for setting up the Velocity runtime |
71 | * <li> createContext() : for creating and loading the Context |
72 | * <li> setContentType() : for changing the content type on a request |
73 | * by request basis |
74 | * <li> handleRequest() : you <b>must</b> implement this |
75 | * <li> mergeTemplate() : the template rendering process |
76 | * <li> requestCleanup() : post rendering resource or other cleanup |
77 | * <li> error() : error handling |
78 | * </ul> |
79 | * <br> |
80 | * If you put a contentType object into the context within either your |
81 | * serlvet or within your template, then that will be used to override |
82 | * the default content type specified in the properties file. |
83 | * |
84 | * "contentType" - The value for the Content-Type: header |
85 | * |
86 | * @author Dave Bryson |
87 | * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a> |
88 | * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> |
89 | * @author <a href="kjohnson@transparent.com">Kent Johnson</a> |
90 | * @author <a href="dlr@finemaltcoding.com">Daniel Rall</a> |
91 | * $Id: VelocityServlet.java,v 1.52.4.1 2004/03/03 23:23:03 geirm Exp $ |
92 | */ |
93 | public abstract class VelocityServlet extends HttpServlet |
94 | { |
95 | /** |
96 | * The context key for the HTTP request object. |
97 | */ |
98 | public static final String REQUEST = "req"; |
99 | |
100 | /** |
101 | * The context key for the HTTP response object. |
102 | */ |
103 | public static final String RESPONSE = "res"; |
104 | |
105 | /** |
106 | * The HTTP content type context key. |
107 | */ |
108 | public static final String CONTENT_TYPE = "default.contentType"; |
109 | |
110 | /** |
111 | * The default content type for the response |
112 | */ |
113 | public static final String DEFAULT_CONTENT_TYPE = "text/html"; |
114 | |
115 | |
116 | /** |
117 | * Encoding for the output stream |
118 | */ |
119 | public static final String DEFAULT_OUTPUT_ENCODING = "ISO-8859-1"; |
120 | |
121 | /** |
122 | * The default content type, itself defaulting to {@link |
123 | * #DEFAULT_CONTENT_TYPE} if not configured. |
124 | */ |
125 | private static String defaultContentType; |
126 | |
127 | /** |
128 | * This is the string that is looked for when getInitParameter is |
129 | * called (<code>org.apache.velocity.properties</code>). |
130 | */ |
131 | protected static final String INIT_PROPS_KEY = |
132 | "org.apache.velocity.properties"; |
133 | |
134 | /** |
135 | * Use of this properties key has been deprecated, and will be |
136 | * removed in Velocity version 1.5. |
137 | */ |
138 | private static final String OLD_INIT_PROPS_KEY = "properties"; |
139 | |
140 | /** |
141 | * Cache of writers |
142 | */ |
143 | |
144 | private static SimplePool writerPool = new SimplePool(40); |
145 | |
146 | /** |
147 | * Performs initialization of this servlet. Called by the servlet |
148 | * container on loading. |
149 | * |
150 | * @param config The servlet configuration to apply. |
151 | * |
152 | * @exception ServletException |
153 | */ |
154 | public void init( ServletConfig config ) |
155 | throws ServletException |
156 | { |
157 | super.init( config ); |
158 | |
159 | /* |
160 | * do whatever we have to do to init Velocity |
161 | */ |
162 | initVelocity( config ); |
163 | |
164 | /* |
165 | * Now that Velocity is initialized, cache some config. |
166 | */ |
167 | defaultContentType = RuntimeSingleton.getString(CONTENT_TYPE, |
168 | DEFAULT_CONTENT_TYPE); |
169 | } |
170 | |
171 | /** |
172 | * Initializes the Velocity runtime, first calling |
173 | * loadConfiguration(ServletConvig) to get a |
174 | * java.util.Properties of configuration information |
175 | * and then calling Velocity.init(). Override this |
176 | * to do anything to the environment before the |
177 | * initialization of the singelton takes place, or to |
178 | * initialize the singleton in other ways. |
179 | */ |
180 | protected void initVelocity( ServletConfig config ) |
181 | throws ServletException |
182 | { |
183 | try |
184 | { |
185 | /* |
186 | * call the overridable method to allow the |
187 | * derived classes a shot at altering the configuration |
188 | * before initializing Runtime |
189 | */ |
190 | |
191 | Properties props = loadConfiguration( config ); |
192 | |
193 | Velocity.init( props ); |
194 | } |
195 | catch( Exception e ) |
196 | { |
197 | throw new ServletException("Error initializing Velocity: " + e, e); |
198 | } |
199 | } |
200 | |
201 | /** |
202 | * Loads the configuration information and returns that |
203 | * information as a Properties, which will be used to |
204 | * initialize the Velocity runtime. |
205 | * <br><br> |
206 | * Currently, this method gets the initialization parameter |
207 | * VelocityServlet.INIT_PROPS_KEY, which should be a file containing |
208 | * the configuration information. |
209 | * <br><br> |
210 | * To configure your Servlet Spec 2.2 compliant servlet runner to pass |
211 | * this to you, put the following in your WEB-INF/web.xml file |
212 | * <br> |
213 | * <pre> |
214 | * <servlet> |
215 | * <servlet-name> YourServlet </servlet-name> |
216 | * <servlet-class> your.package.YourServlet </servlet-class> |
217 | * <init-param> |
218 | * <param-name> org.apache.velocity.properties </param-name> |
219 | * <param-value> velocity.properties </param-value> |
220 | * </init-param> |
221 | * </servlet> |
222 | * </pre> |
223 | * |
224 | * Alternately, if you wish to configure an entire context in this |
225 | * fashion, you may use the following: |
226 | * <br> |
227 | * <pre> |
228 | * <context-param> |
229 | * <param-name> org.apache.velocity.properties </param-name> |
230 | * <param-value> velocity.properties </param-value> |
231 | * <description> Path to Velocity configuration </description> |
232 | * </context-param> |
233 | * </pre> |
234 | * |
235 | * Derived classes may do the same, or take advantage of this code to do the loading for them via : |
236 | * <pre> |
237 | * Properties p = super.loadConfiguration( config ); |
238 | * </pre> |
239 | * and then add or modify the configuration values from the file. |
240 | * <br> |
241 | * |
242 | * @param config ServletConfig passed to the servlets init() function |
243 | * Can be used to access the real path via ServletContext (hint) |
244 | * @return java.util.Properties loaded with configuration values to be used |
245 | * to initialize the Velocity runtime. |
246 | * @throws FileNotFoundException if a specified file is not found. |
247 | * @throws IOException I/O problem accessing the specified file, if specified. |
248 | */ |
249 | protected Properties loadConfiguration(ServletConfig config) |
250 | throws IOException, FileNotFoundException |
251 | { |
252 | // This is a little overly complex because of legacy support |
253 | // for the initialization properties key "properties". |
254 | // References to OLD_INIT_PROPS_KEY should be removed at |
255 | // Velocity version 1.5. |
256 | String propsFile = config.getInitParameter(INIT_PROPS_KEY); |
257 | if (propsFile == null || propsFile.length() == 0) |
258 | { |
259 | ServletContext sc = config.getServletContext(); |
260 | propsFile = config.getInitParameter(OLD_INIT_PROPS_KEY); |
261 | if (propsFile == null || propsFile.length() == 0) |
262 | { |
263 | propsFile = sc.getInitParameter(INIT_PROPS_KEY); |
264 | if (propsFile == null || propsFile.length() == 0) |
265 | { |
266 | propsFile = sc.getInitParameter(OLD_INIT_PROPS_KEY); |
267 | if (propsFile != null && propsFile.length() > 0) |
268 | { |
269 | sc.log("Use of the properties initialization " + |
270 | "parameter '" + OLD_INIT_PROPS_KEY + "' has " + |
271 | "been deprecated by '" + INIT_PROPS_KEY + '\''); |
272 | } |
273 | } |
274 | } |
275 | else |
276 | { |
277 | sc.log("Use of the properties initialization parameter '" + |
278 | OLD_INIT_PROPS_KEY + "' has been deprecated by '" + |
279 | INIT_PROPS_KEY + '\''); |
280 | } |
281 | } |
282 | |
283 | /* |
284 | * This will attempt to find the location of the properties |
285 | * file from the relative path to the WAR archive (ie: |
286 | * docroot). Since JServ returns null for getRealPath() |
287 | * because it was never implemented correctly, then we know we |
288 | * will not have an issue with using it this way. I don't know |
289 | * if this will break other servlet engines, but it probably |
290 | * shouldn't since WAR files are the future anyways. |
291 | */ |
292 | |
293 | Properties p = new Properties(); |
294 | |
295 | if ( propsFile != null ) |
296 | { |
297 | String realPath = getServletContext().getRealPath(propsFile); |
298 | |
299 | if ( realPath != null ) |
300 | { |
301 | propsFile = realPath; |
302 | } |
303 | |
304 | p.load( new FileInputStream(propsFile) ); |
305 | } |
306 | |
307 | return p; |
308 | } |
309 | |
310 | /** |
311 | * Handles HTTP <code>GET</code> requests by calling {@link |
312 | * #doRequest()}. |
313 | */ |
314 | public void doGet( HttpServletRequest request, HttpServletResponse response ) |
315 | throws ServletException, IOException |
316 | { |
317 | doRequest(request, response); |
318 | } |
319 | |
320 | /** |
321 | * Handles HTTP <code>POST</code> requests by calling {@link |
322 | * #doRequest()}. |
323 | */ |
324 | public void doPost( HttpServletRequest request, HttpServletResponse response ) |
325 | throws ServletException, IOException |
326 | { |
327 | doRequest(request, response); |
328 | } |
329 | |
330 | /** |
331 | * Handles all requests (by default). |
332 | * |
333 | * @param request HttpServletRequest object containing client request |
334 | * @param response HttpServletResponse object for the response |
335 | */ |
336 | protected void doRequest(HttpServletRequest request, HttpServletResponse response ) |
337 | throws ServletException, IOException |
338 | { |
339 | Context context = null; |
340 | try |
341 | { |
342 | /* |
343 | * first, get a context |
344 | */ |
345 | |
346 | context = createContext( request, response ); |
347 | |
348 | /* |
349 | * set the content type |
350 | */ |
351 | |
352 | setContentType( request, response ); |
353 | |
354 | /* |
355 | * let someone handle the request |
356 | */ |
357 | |
358 | Template template = handleRequest( request, response, context ); |
359 | /* |
360 | * bail if we can't find the template |
361 | */ |
362 | |
363 | if ( template == null ) |
364 | { |
365 | return; |
366 | } |
367 | |
368 | /* |
369 | * now merge it |
370 | */ |
371 | |
372 | mergeTemplate( template, context, response ); |
373 | } |
374 | catch (Exception e) |
375 | { |
376 | /* |
377 | * call the error handler to let the derived class |
378 | * do something useful with this failure. |
379 | */ |
380 | |
381 | error( request, response, e); |
382 | } |
383 | finally |
384 | { |
385 | /* |
386 | * call cleanup routine to let a derived class do some cleanup |
387 | */ |
388 | |
389 | requestCleanup( request, response, context ); |
390 | } |
391 | } |
392 | |
393 | /** |
394 | * A cleanup routine which is called at the end of the {@link |
395 | * #doRequest(HttpServletRequest, HttpServletResponse)} |
396 | * processing sequence, allowing a derived class to do resource |
397 | * cleanup or other end of process cycle tasks. |
398 | * |
399 | * @param request servlet request from client |
400 | * @param response servlet reponse |
401 | * @param context context created by the createContext() method |
402 | */ |
403 | protected void requestCleanup( HttpServletRequest request, HttpServletResponse response, Context context ) |
404 | { |
405 | return; |
406 | } |
407 | |
408 | /** |
409 | * merges the template with the context. Only override this if you really, really |
410 | * really need to. (And don't call us with questions if it breaks :) |
411 | * |
412 | * @param template template object returned by the handleRequest() method |
413 | * @param context context created by the createContext() method |
414 | * @param response servlet reponse (use this to get the output stream or Writer |
415 | */ |
416 | protected void mergeTemplate( Template template, Context context, HttpServletResponse response ) |
417 | throws ResourceNotFoundException, ParseErrorException, |
418 | MethodInvocationException, IOException, UnsupportedEncodingException, Exception |
419 | { |
420 | ServletOutputStream output = response.getOutputStream(); |
421 | VelocityWriter vw = null; |
422 | // ASSUMPTION: response.setContentType() has been called. |
423 | String encoding = response.getCharacterEncoding(); |
424 | |
425 | try |
426 | { |
427 | vw = (VelocityWriter) writerPool.get(); |
428 | |
429 | if (vw == null) |
430 | { |
431 | vw = new VelocityWriter(new OutputStreamWriter(output, |
432 | encoding), |
433 | 4 * 1024, true); |
434 | } |
435 | else |
436 | { |
437 | vw.recycle(new OutputStreamWriter(output, encoding)); |
438 | } |
439 | |
440 | template.merge(context, vw); |
441 | } |
442 | finally |
443 | { |
444 | try |
445 | { |
446 | if (vw != null) |
447 | { |
448 | /* |
449 | * flush and put back into the pool |
450 | * don't close to allow us to play |
451 | * nicely with others. |
452 | */ |
453 | vw.flush(); |
454 | |
455 | /* |
456 | * Clear the VelocityWriter's reference to its |
457 | * internal OutputStreamWriter to allow the latter |
458 | * to be GC'd while vw is pooled. |
459 | */ |
460 | vw.recycle(null); |
461 | |
462 | writerPool.put(vw); |
463 | } |
464 | } |
465 | catch (Exception e) |
466 | { |
467 | // do nothing |
468 | } |
469 | } |
470 | } |
471 | |
472 | /** |
473 | * Sets the content type of the response, defaulting to {@link |
474 | * #defaultContentType} if not overriden. Delegates to {@link |
475 | * #chooseCharacterEncoding(HttpServletRequest)} to select the |
476 | * appropriate character encoding. |
477 | * |
478 | * @param request The servlet request from the client. |
479 | * @param response The servlet reponse to the client. |
480 | */ |
481 | protected void setContentType(HttpServletRequest request, |
482 | HttpServletResponse response) |
483 | { |
484 | String contentType = defaultContentType; |
485 | int index = contentType.lastIndexOf(';') + 1; |
486 | if (index <= 0 || (index < contentType.length() && |
487 | contentType.indexOf("charset", index) == -1)) |
488 | { |
489 | // Append the character encoding which we'd like to use. |
490 | String encoding = chooseCharacterEncoding(request); |
491 | //System.out.println("Chose output encoding of '" + |
492 | // encoding + '\''); |
493 | if (!DEFAULT_OUTPUT_ENCODING.equalsIgnoreCase(encoding)) |
494 | { |
495 | contentType += "; charset=" + encoding; |
496 | } |
497 | } |
498 | response.setContentType(contentType); |
499 | //System.out.println("Response Content-Type set to '" + |
500 | // contentType + '\''); |
501 | } |
502 | |
503 | /** |
504 | * Chooses the output character encoding to be used as the value |
505 | * for the "charset=" portion of the HTTP Content-Type header (and |
506 | * thus returned by <code>response.getCharacterEncoding()</code>). |
507 | * Called by {@link #setContentType(HttpServletRequest, |
508 | * HttpServletResponse)} if an encoding isn't already specified by |
509 | * Content-Type. By default, chooses the value of |
510 | * RuntimeSingleton's <code>output.encoding</code> property. |
511 | * |
512 | * @param request The servlet request from the client. |
513 | */ |
514 | protected String chooseCharacterEncoding(HttpServletRequest request) |
515 | { |
516 | return RuntimeSingleton.getString(RuntimeConstants.OUTPUT_ENCODING, |
517 | DEFAULT_OUTPUT_ENCODING); |
518 | } |
519 | |
520 | /** |
521 | * Returns a context suitable to pass to the handleRequest() method |
522 | * <br><br> |
523 | * Default implementation will create a VelocityContext object, |
524 | * put the HttpServletRequest and HttpServletResponse |
525 | * into the context accessable via the keys VelocityServlet.REQUEST and |
526 | * VelocityServlet.RESPONSE, respectively. |
527 | * |
528 | * @param request servlet request from client |
529 | * @param response servlet reponse to client |
530 | * |
531 | * @return context |
532 | */ |
533 | protected Context createContext(HttpServletRequest request, HttpServletResponse response ) |
534 | { |
535 | /* |
536 | * create a new context |
537 | */ |
538 | |
539 | VelocityContext context = new VelocityContext(); |
540 | |
541 | /* |
542 | * put the request/response objects into the context |
543 | * wrap the HttpServletRequest to solve the introspection |
544 | * problems |
545 | */ |
546 | |
547 | context.put( REQUEST, request ); |
548 | context.put( RESPONSE, response ); |
549 | |
550 | return context; |
551 | } |
552 | |
553 | /** |
554 | * Retrieves the requested template. |
555 | * |
556 | * @param name The file name of the template to retrieve relative to the |
557 | * template root. |
558 | * @return The requested template. |
559 | * @throws ResourceNotFoundException if template not found |
560 | * from any available source. |
561 | * @throws ParseErrorException if template cannot be parsed due |
562 | * to syntax (or other) error. |
563 | * @throws Exception if an error occurs in template initialization |
564 | */ |
565 | public Template getTemplate( String name ) |
566 | throws ResourceNotFoundException, ParseErrorException, Exception |
567 | { |
568 | return RuntimeSingleton.getTemplate(name); |
569 | } |
570 | |
571 | /** |
572 | * Retrieves the requested template with the specified |
573 | * character encoding. |
574 | * |
575 | * @param name The file name of the template to retrieve relative to the |
576 | * template root. |
577 | * @param encoding the character encoding of the template |
578 | * |
579 | * @return The requested template. |
580 | * @throws ResourceNotFoundException if template not found |
581 | * from any available source. |
582 | * @throws ParseErrorException if template cannot be parsed due |
583 | * to syntax (or other) error. |
584 | * @throws Exception if an error occurs in template initialization |
585 | * |
586 | * @since Velocity v1.1 |
587 | */ |
588 | public Template getTemplate( String name, String encoding ) |
589 | throws ResourceNotFoundException, ParseErrorException, Exception |
590 | { |
591 | return RuntimeSingleton.getTemplate( name, encoding ); |
592 | } |
593 | |
594 | /** |
595 | * Implement this method to add your application data to the context, |
596 | * calling the <code>getTemplate()</code> method to produce your return |
597 | * value. |
598 | * <br><br> |
599 | * In the event of a problem, you may handle the request directly |
600 | * and return <code>null</code> or throw a more meaningful exception |
601 | * for the error handler to catch. |
602 | * |
603 | * @param request servlet request from client |
604 | * @param response servlet reponse |
605 | * @param ctx The context to add your data to. |
606 | * @return The template to merge with your context or null, indicating |
607 | * that you handled the processing. |
608 | * |
609 | * @since Velocity v1.1 |
610 | */ |
611 | protected Template handleRequest( HttpServletRequest request, HttpServletResponse response, Context ctx ) |
612 | throws Exception |
613 | { |
614 | /* |
615 | * invoke handleRequest |
616 | */ |
617 | |
618 | Template t = handleRequest( ctx ); |
619 | |
620 | /* |
621 | * if it returns null, this is the 'old' deprecated |
622 | * way, and we want to mimic the behavior for a little |
623 | * while anyway |
624 | */ |
625 | |
626 | if (t == null) |
627 | { |
628 | throw new Exception ("handleRequest(Context) returned null - no template selected!" ); |
629 | } |
630 | |
631 | return t; |
632 | } |
633 | |
634 | /** |
635 | * Implement this method to add your application data to the context, |
636 | * calling the <code>getTemplate()</code> method to produce your return |
637 | * value. |
638 | * <br><br> |
639 | * In the event of a problem, you may simple return <code>null</code> |
640 | * or throw a more meaningful exception. |
641 | * |
642 | * @deprecated Use |
643 | * {@link #handleRequest( HttpServletRequest request, |
644 | * HttpServletResponse response, Context ctx )} |
645 | * |
646 | * @param ctx The context to add your data to. |
647 | * @return The template to merge with your context. |
648 | */ |
649 | protected Template handleRequest( Context ctx ) |
650 | throws Exception |
651 | { |
652 | throw new Exception ("You must override VelocityServlet.handleRequest( Context) " |
653 | + " or VelocityServlet.handleRequest( HttpServletRequest, " |
654 | + " HttpServletResponse, Context)" ); |
655 | } |
656 | |
657 | /** |
658 | * Invoked when there is an error thrown in any part of doRequest() processing. |
659 | * <br><br> |
660 | * Default will send a simple HTML response indicating there was a problem. |
661 | * |
662 | * @param request original HttpServletRequest from servlet container. |
663 | * @param response HttpServletResponse object from servlet container. |
664 | * @param cause Exception that was thrown by some other part of process. |
665 | */ |
666 | protected void error( HttpServletRequest request, HttpServletResponse response, Exception cause ) |
667 | throws ServletException, IOException |
668 | { |
669 | StringBuffer html = new StringBuffer(); |
670 | html.append("<html>"); |
671 | html.append("<title>Error</title>"); |
672 | html.append("<body bgcolor=\"#ffffff\">"); |
673 | html.append("<h2>VelocityServlet: Error processing the template</h2>"); |
674 | html.append("<pre>"); |
675 | String why = cause.getMessage(); |
676 | if (why != null && why.trim().length() > 0) |
677 | { |
678 | html.append(why); |
679 | html.append("<br>"); |
680 | } |
681 | |
682 | StringWriter sw = new StringWriter(); |
683 | cause.printStackTrace( new PrintWriter( sw ) ); |
684 | |
685 | html.append( sw.toString() ); |
686 | html.append("</pre>"); |
687 | html.append("</body>"); |
688 | html.append("</html>"); |
689 | response.getOutputStream().print( html.toString() ); |
690 | } |
691 | } |