1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.jetspeed.aggregator.impl;
18
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.Iterator;
22 import java.util.Map;
23 import java.util.List;
24
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
27
28 import org.apache.commons.lang.StringEscapeUtils;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.apache.jetspeed.JetspeedActions;
32 import org.apache.jetspeed.PortalReservedParameters;
33 import org.apache.jetspeed.aggregator.ContentDispatcher;
34 import org.apache.jetspeed.aggregator.ContentDispatcherCtrl;
35 import org.apache.jetspeed.aggregator.FailedToRenderFragmentException;
36 import org.apache.jetspeed.aggregator.PortletAccessDeniedException;
37 import org.apache.jetspeed.aggregator.PortletContent;
38 import org.apache.jetspeed.aggregator.PortletRenderer;
39 import org.apache.jetspeed.aggregator.PortletTrackingManager;
40 import org.apache.jetspeed.aggregator.RenderingJob;
41 import org.apache.jetspeed.aggregator.UnknownPortletDefinitionException;
42 import org.apache.jetspeed.aggregator.WorkerMonitor;
43 import org.apache.jetspeed.cache.CacheElement;
44 import org.apache.jetspeed.cache.ContentCacheKey;
45 import org.apache.jetspeed.cache.JetspeedCache;
46 import org.apache.jetspeed.components.portletentity.PortletEntityNotStoredException;
47 import org.apache.jetspeed.container.window.FailedToRetrievePortletWindow;
48 import org.apache.jetspeed.container.window.PortletWindowAccessor;
49 import org.apache.jetspeed.om.common.LocalizedField;
50 import org.apache.jetspeed.om.common.portlet.MutablePortletEntity;
51 import org.apache.jetspeed.om.common.portlet.PortletDefinitionComposite;
52 import org.apache.jetspeed.om.page.ContentFragment;
53 import org.apache.jetspeed.om.window.impl.PortletWindowImpl;
54 import org.apache.jetspeed.request.RequestContext;
55 import org.apache.jetspeed.security.SecurityAccessController;
56 import org.apache.jetspeed.services.title.DynamicTitleService;
57 import org.apache.jetspeed.statistics.PortalStatistics;
58 import org.apache.pluto.PortletContainer;
59 import org.apache.pluto.om.entity.PortletEntity;
60 import org.apache.pluto.om.window.PortletWindow;
61
62 /***
63 * <h4>PortletRendererService <br />
64 * Jetspeed-2 Rendering service.</h4>
65 * <p>
66 * This service process all portlet rendering requests and interfaces with the
67 * portlet container to generate the resulting markup
68 * </p>
69 *
70 * @author <a href="mailto:raphael@apache.org">Rapha?l Luta </a>
71 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
72 * @author <a>Woonsan Ko</a>
73 * @version $Id: PortletRendererImpl.java,v 1.30 2005/05/20 14:54:22 ate Exp $
74 */
75 public class PortletRendererImpl implements PortletRenderer
76 {
77 protected final static Log log = LogFactory.getLog(PortletRendererImpl.class);
78
79 protected WorkerMonitor workMonitor;
80 protected PortletContainer container;
81 protected PortletWindowAccessor windowAccessor;
82 protected PortalStatistics statistics;
83 protected DynamicTitleService addTitleService;
84
85 protected PortletTrackingManager portletTracking;
86
87 /***
88 * flag indicating whether to check jetspeed-portlet.xml security constraints
89 * before rendering a portlet. If security check fails, do not display portlet content
90 */
91 protected boolean checkSecurityConstraints;
92 /***
93 * For security constraint checks
94 */
95 protected SecurityAccessController accessController;
96
97 /***
98 * JSR 168 Portlet Content Cache
99 */
100 protected JetspeedCache portletContentCache;
101
102 /***
103 * OutOfService Cache
104 */
105 protected boolean overrideTitles = false;
106 public static final String OUT_OF_SERVICE_MESSAGE = "Portlet is not responding and has been taken out of service.";
107
108 public PortletRendererImpl(PortletContainer container,
109 PortletWindowAccessor windowAccessor,
110 WorkerMonitor workMonitor,
111 PortalStatistics statistics,
112 DynamicTitleService addTitleService,
113 PortletTrackingManager portletTracking,
114 boolean checkSecurityConstraints,
115 SecurityAccessController accessController,
116 JetspeedCache portletContentCache,
117 boolean overrideTitles)
118 {
119 this.container = container;
120 this.windowAccessor = windowAccessor;
121 this.workMonitor = workMonitor;
122 this.statistics = statistics;
123 this.addTitleService = addTitleService;
124 this.portletTracking = portletTracking;
125 this.checkSecurityConstraints = checkSecurityConstraints;
126 this.accessController = accessController;
127 this.portletContentCache = portletContentCache;
128 this.overrideTitles = overrideTitles;
129 }
130
131 public PortletRendererImpl(PortletContainer container,
132 PortletWindowAccessor windowAccessor,
133 WorkerMonitor workMonitor,
134 PortalStatistics statistics,
135 DynamicTitleService addTitleService,
136 PortletTrackingManager portletTracking,
137 boolean checkSecurityConstraints,
138 SecurityAccessController accessController,
139 JetspeedCache portletContentCache)
140 {
141 this(container, windowAccessor, workMonitor, statistics,
142 addTitleService, portletTracking, checkSecurityConstraints,
143 accessController, portletContentCache, false);
144 }
145
146 public PortletRendererImpl(PortletContainer container,
147 PortletWindowAccessor windowAccessor,
148 WorkerMonitor workMonitor,
149 PortalStatistics statistics,
150 DynamicTitleService addTitleService)
151 {
152 this(container, windowAccessor, workMonitor, statistics, null, null, false, null, null, true);
153 }
154
155 public PortletRendererImpl(PortletContainer container,
156 PortletWindowAccessor windowAccessor,
157 WorkerMonitor workMonitor,
158 PortalStatistics statistics)
159 {
160 this( container, windowAccessor, workMonitor, statistics, null );
161 }
162
163 public PortletRendererImpl(PortletContainer container,
164 PortletWindowAccessor windowAccessor,
165 WorkerMonitor workMonitor)
166 {
167 this( container, windowAccessor, workMonitor, null );
168 }
169
170 public void start()
171 {
172
173 }
174
175 public void stop()
176 {
177
178 }
179
180 /***
181 * Render the specified Page fragment. Result is returned in the
182 * PortletResponse.
183 *
184 * @throws FailedToRenderFragmentException
185 * @throws FailedToRetrievePortletWindow
186 * @throws UnknownPortletDefinitionException
187 */
188 public void renderNow( ContentFragment fragment, RequestContext requestContext )
189 {
190 HttpServletRequest servletRequest =null;
191 HttpServletResponse servletResponse = null;
192 ContentDispatcherCtrl dispatcher = null;
193 boolean contentIsCached = false;
194 try
195 {
196 PortletWindow portletWindow = getPortletWindow(fragment);
197 PortletDefinitionComposite portletDefinition =
198 (PortletDefinitionComposite) portletWindow.getPortletEntity().getPortletDefinition();
199 if (checkSecurityConstraints && !checkSecurityConstraint(portletDefinition, fragment))
200 {
201 throw new PortletAccessDeniedException("Access Denied.");
202 }
203 if (portletTracking.isOutOfService(portletWindow))
204 {
205 log.info("Taking portlet out of service: " + portletDefinition.getUniqueName() + " for window " + fragment.getId());
206 fragment.overrideRenderedContent(OUT_OF_SERVICE_MESSAGE);
207 return;
208 }
209 long timeoutMetadata = this.getTimeoutOnJob(portletDefinition);
210 portletTracking.setExpiration(portletWindow, timeoutMetadata);
211 int expirationCache = getExpirationCache(portletDefinition);
212 if (expirationCache != 0)
213 {
214 if (retrieveCachedContent(requestContext, fragment, portletWindow, expirationCache, portletDefinition))
215 return;
216 contentIsCached = true;
217 }
218 if (dispatcher == null)
219 {
220 dispatcher = createDispatcher(requestContext, fragment, expirationCache);
221 }
222 servletRequest = requestContext.getRequestForWindow(portletWindow);
223 servletResponse = dispatcher.getResponseForWindow(portletWindow, requestContext);
224 RenderingJob rJob =
225 buildRenderingJob(portletWindow, fragment, servletRequest, servletResponse,
226 requestContext, false, portletDefinition, dispatcher, null,
227 expirationCache, contentIsCached, timeoutMetadata);
228 rJob.execute();
229 addTitleToHeader( portletWindow, fragment, servletRequest, servletResponse, dispatcher, contentIsCached);
230 }
231 catch (PortletAccessDeniedException e)
232 {
233 fragment.overrideRenderedContent(e.getLocalizedMessage());
234 }
235 catch (Exception e)
236 {
237 fragment.overrideRenderedContent(e.getLocalizedMessage());
238 log.error(e.toString(), e);
239 }
240 }
241
242 /***
243 * Render the specified Page fragment. Result is returned in the
244 * PortletResponse.
245 *
246 * @throws FailedToRenderFragmentException
247 * @throws FailedToRetrievePortletWindow
248 * @throws UnknownPortletDefinitionException
249 * @throws PortletAccessDeniedException
250 */
251 public void renderNow( ContentFragment fragment, HttpServletRequest request, HttpServletResponse response )
252 {
253 RequestContext requestContext = (RequestContext) request
254 .getAttribute(PortalReservedParameters.REQUEST_CONTEXT_ATTRIBUTE);
255 renderNow(fragment, requestContext);
256 }
257
258 protected int getExpirationCache(PortletDefinitionComposite portletDefinition)
259 {
260 if (portletDefinition == null)
261 return 0;
262 String expiration = portletDefinition.getExpirationCache();
263 if (expiration == null)
264 return 0;
265 return Integer.parseInt(expiration);
266 }
267
268 /***
269 * Render the specified Page fragment. The method returns before rendering
270 * is complete, rendered content can be accessed through the Content Dispatcher
271 *
272 * @return the asynchronous portlet rendering job to synchronize
273 */
274 public RenderingJob render( ContentFragment fragment, RequestContext requestContext )
275 {
276 RenderingJob job = null;
277
278 try
279 {
280 job = createRenderingJob(fragment, requestContext);
281 }
282 catch (Exception e)
283 {
284 log.error("render() failed: " + e.toString(), e);
285 fragment.overrideRenderedContent(e.getLocalizedMessage());
286 }
287
288 if (job != null)
289 {
290 processRenderingJob(job, true);
291 }
292
293 return job;
294 }
295
296 /***
297 *
298 * Create a rendering job for the specified Page fragment.
299 * The method returns a rendering job which should be passed to 'processRenderingJob(RenderingJob job)' method.
300 * @return portlet rendering job to pass to render(RenderingJob job) method
301 * @throws FailedToRetrievePortletWindow
302 * @throws UnknownPortletDefinitionException
303 * @throws PortletAccessDeniedException
304 */
305 public RenderingJob createRenderingJob(ContentFragment fragment, RequestContext requestContext)
306 {
307 RenderingJob job = null;
308 boolean contentIsCached = false;
309 try
310 {
311 PortletWindow portletWindow = getPortletWindow(fragment);
312 PortletDefinitionComposite portletDefinition =
313 (PortletDefinitionComposite) portletWindow.getPortletEntity().getPortletDefinition();
314
315 long timeoutMetadata = this.getTimeoutOnJob(portletDefinition);
316 portletTracking.setExpiration(portletWindow, timeoutMetadata);
317
318 if (checkSecurityConstraints && !checkSecurityConstraint(portletDefinition, fragment))
319 {
320 throw new PortletAccessDeniedException("Access Denied.");
321 }
322 if (portletTracking.isOutOfService(portletWindow))
323 {
324 fragment.overrideRenderedContent(OUT_OF_SERVICE_MESSAGE);
325 return null;
326 }
327 int expirationCache = getExpirationCache(portletDefinition);
328 if (expirationCache != 0)
329 {
330 portletTracking.setExpiration(portletWindow, expirationCache);
331 contentIsCached = retrieveCachedContent(requestContext, fragment, portletWindow,
332 expirationCache, portletDefinition);
333 if (contentIsCached)
334 {
335 return null;
336 }
337 }
338 job = buildRenderingJob( portletWindow, fragment, requestContext, true,
339 portletDefinition, null, contentIsCached, timeoutMetadata );
340 }
341 catch (Exception e)
342 {
343 throw new RuntimeException("Failed to create rendering job", e);
344 }
345
346 return job;
347 }
348
349 /***
350 *
351 * Render the specified rendering job.
352 * The method returns before rendering is complete when the job is processed in parallel mode.
353 * When it is not parallel mode, it returns after rendering is complete.
354 * @throws FailedToRenderFragmentException
355 */
356 public void processRenderingJob(RenderingJob job)
357 {
358 processRenderingJob(job, false);
359 }
360
361 protected void processRenderingJob(RenderingJob job, boolean parallelOnly)
362 {
363 ContentFragment fragment = null;
364
365 try
366 {
367 if (parallelOnly || job.getTimeout() > 0)
368 {
369 workMonitor.process(job);
370 }
371 else
372 {
373 job.execute();
374 addTitleToHeader(job.getWindow(), job.getFragment(),
375 job.getRequest(), job.getResponse(), job.getDispatcher(),
376 job.isContentCached());
377 }
378 }
379 catch (Exception e1)
380 {
381 log.error("render() failed: " + e1.toString(), e1);
382 fragment.overrideRenderedContent(e1.getLocalizedMessage());
383 }
384 }
385
386 /***
387 * Wait for all rendering jobs in the collection to finish successfully or otherwise.
388 * @param renderingJobs the Collection of rendering job objects to wait for.
389 */
390 public void waitForRenderingJobs(List renderingJobs)
391 {
392 this.workMonitor.waitForRenderingJobs(renderingJobs);
393 }
394
395 /***
396 * Retrieve cached content, if content retrieved successfully return true, if no content found return false
397 * @param requestContext
398 * @param fragment
399 * @param portletWindow
400 * @return true when content found, otherwise false
401 */
402 protected boolean retrieveCachedContent(RequestContext requestContext, ContentFragment fragment,
403 PortletWindow portletWindow, int expiration,
404 PortletDefinitionComposite portletDefinition)
405 throws Exception
406 {
407 ContentCacheKey cacheKey = portletContentCache.createCacheKey(requestContext, fragment.getId());
408 CacheElement cachedElement = portletContentCache.get(cacheKey);
409 if (cachedElement != null)
410 {
411 PortletContent portletContent = (PortletContent)cachedElement.getContent();
412 fragment.setPortletContent(portletContent);
413 ContentDispatcherCtrl dispatcher = new ContentDispatcherImpl(portletContent);
414 HttpServletRequest servletRequest = requestContext.getRequestForWindow(portletWindow);
415
416 this.addTitleService.setDynamicTitle(portletWindow, servletRequest, dispatcher.getPortletContent(fragment).getTitle());
417 return true;
418 }
419 return false;
420 }
421
422 public ContentDispatcherCtrl createDispatcher(RequestContext request, ContentFragment fragment, int expirationCache)
423 {
424 ContentCacheKey cacheKey = portletContentCache.createCacheKey(request, fragment.getId());
425 PortletContent content = new PortletContentImpl(this, cacheKey, expirationCache);
426 ContentDispatcherCtrl dispatcher = new ContentDispatcherImpl(content);
427 return dispatcher;
428 }
429
430 /***
431 * Retrieve the ContentDispatcher for the specified request
432 */
433 public ContentDispatcher getDispatcher( RequestContext request, boolean isParallel )
434 {
435 return request.getContentDispatcher();
436 }
437
438
439 protected PortletWindow getPortletWindow( ContentFragment fragment ) throws FailedToRetrievePortletWindow, PortletEntityNotStoredException
440 {
441
442 PortletWindow portletWindow = windowAccessor.getPortletWindow(fragment);
443
444 if (portletWindow == null)
445 {
446 throw new FailedToRetrievePortletWindow("Portlet Window creation failed for fragment: "
447 + fragment.getId() + ", " + fragment.getName());
448 }
449
450 PortletEntity portletEntity = portletWindow.getPortletEntity();
451 ((MutablePortletEntity)portletEntity).setFragment(fragment);
452
453 ((PortletWindowImpl) portletWindow).setInstantlyRendered(fragment.isInstantlyRendered());
454
455 return portletWindow;
456 }
457
458 protected RenderingJob buildRenderingJob( PortletWindow portletWindow, ContentFragment fragment,
459 RequestContext requestContext, boolean isParallel,
460 PortletDefinitionComposite portletDefinition,
461 PortletContent portletContent, boolean contentIsCached, long timeoutMetadata)
462 throws PortletAccessDeniedException, FailedToRetrievePortletWindow, PortletEntityNotStoredException
463 {
464 int expirationCache = getExpirationCache(portletDefinition);
465 ContentDispatcherCtrl dispatcher = createDispatcher(requestContext, fragment, expirationCache);
466 HttpServletRequest request = requestContext.getRequestForWindow(portletWindow);
467 HttpServletResponse response = dispatcher.getResponseForWindow(portletWindow, requestContext);
468
469 return buildRenderingJob( portletWindow, fragment, request, response,
470 requestContext, isParallel,
471 portletDefinition, dispatcher,
472 portletContent, expirationCache, contentIsCached, timeoutMetadata );
473 }
474
475 protected RenderingJob buildRenderingJob( PortletWindow portletWindow, ContentFragment fragment,
476 HttpServletRequest request, HttpServletResponse response,
477 RequestContext requestContext, boolean isParallel,
478 PortletDefinitionComposite portletDefinition,
479 ContentDispatcherCtrl dispatcher,
480 PortletContent portletContent,
481 int expirationCache, boolean contentIsCached, long timeoutMetadata)
482 throws PortletAccessDeniedException, FailedToRetrievePortletWindow, PortletEntityNotStoredException
483 {
484 RenderingJob rJob = null;
485
486 request.setAttribute(PortalReservedParameters.PAGE_ATTRIBUTE, requestContext.getPage());
487 request.setAttribute(PortalReservedParameters.FRAGMENT_ATTRIBUTE, fragment);
488 request.setAttribute(PortalReservedParameters.CONTENT_DISPATCHER_ATTRIBUTE, dispatcher);
489 request.setAttribute(PortalReservedParameters.REQUEST_CONTEXT_ATTRIBUTE, requestContext);
490 request.setAttribute(PortalReservedParameters.REQUEST_CONTEXT_OBJECTS, requestContext.getObjects());
491 request.setAttribute(PortalReservedParameters.PATH_ATTRIBUTE, requestContext.getAttribute(PortalReservedParameters.PATH_ATTRIBUTE));
492 request.setAttribute(PortalReservedParameters.PORTLET_WINDOW_ATTRIBUTE, portletWindow);
493
494 if (portletContent == null)
495 {
496 portletContent = dispatcher.getPortletContent(fragment);
497 fragment.setPortletContent(portletContent);
498 }
499
500 if (isParallel)
501 {
502 Map workerAttrs = new HashMap();
503 workerAttrs.put(PortalReservedParameters.PAGE_ATTRIBUTE, requestContext.getPage());
504 workerAttrs.put(PortalReservedParameters.FRAGMENT_ATTRIBUTE, fragment);
505 workerAttrs.put(PortalReservedParameters.CONTENT_DISPATCHER_ATTRIBUTE, dispatcher);
506 workerAttrs.put(PortalReservedParameters.REQUEST_CONTEXT_ATTRIBUTE, requestContext);
507 workerAttrs.put(PortalReservedParameters.REQUEST_CONTEXT_OBJECTS, requestContext.getObjects());
508 workerAttrs.put(PortalReservedParameters.PATH_ATTRIBUTE, requestContext.getAttribute(PortalReservedParameters.PATH_ATTRIBUTE));
509 workerAttrs.put(PortalReservedParameters.PORTLET_WINDOW_ATTRIBUTE, portletWindow);
510
511
512
513 workerAttrs.put(PortalReservedParameters.PORTLET_DEFINITION_ATTRIBUTE, portletDefinition);
514
515 rJob = new RenderingJobImpl(container, this, portletDefinition, portletContent, fragment, dispatcher,
516 request, response, requestContext, portletWindow,
517 statistics, expirationCache, contentIsCached, workerAttrs);
518
519 }
520 else
521 {
522 rJob = new RenderingJobImpl(container, this, portletDefinition, portletContent, fragment, dispatcher,
523 request, response, requestContext, portletWindow,
524 statistics, expirationCache, contentIsCached );
525
526 }
527
528 if (isParallel)
529 {
530 setTimeoutOnJob(timeoutMetadata, rJob);
531 }
532
533 return rJob;
534 }
535
536 protected long getTimeoutOnJob(PortletDefinitionComposite portletDefinition)
537 {
538 long timeoutMetadata = 0;
539 Collection timeoutFields = null;
540
541 if (portletDefinition != null)
542 {
543 timeoutFields = portletDefinition.getMetadata().getFields(PortalReservedParameters.PORTLET_EXTENDED_DESCRIPTOR_RENDER_TIMEOUT);
544 }
545
546 if (timeoutFields != null)
547 {
548 Iterator it = timeoutFields.iterator();
549
550 if (it.hasNext())
551 {
552 LocalizedField timeoutField = (LocalizedField) timeoutFields.iterator().next();
553
554 try
555 {
556 timeoutMetadata = Long.parseLong(timeoutField.getValue());
557 }
558 catch (NumberFormatException nfe)
559 {
560 log.warn("Invalid timeout metadata: " + nfe.getMessage());
561 }
562 }
563 }
564 return timeoutMetadata;
565 }
566
567 protected void setTimeoutOnJob(long timeoutMetadata, RenderingJob rJob)
568 {
569
570 if (timeoutMetadata > 0)
571 {
572 rJob.setTimeout(timeoutMetadata);
573 }
574 else if (this.portletTracking.getDefaultPortletTimeout() > 0)
575 {
576 rJob.setTimeout(this.portletTracking.getDefaultPortletTimeout());
577 }
578 }
579
580 public void addTitleToHeader( PortletWindow portletWindow, ContentFragment fragment,
581 HttpServletRequest request, HttpServletResponse response,
582 ContentDispatcherCtrl dispatcher, boolean isCacheTitle )
583 {
584 if (overrideTitles)
585 {
586 try
587 {
588 String title = fragment.getTitle();
589
590 if ( title == null )
591 {
592 title = addTitleService.getDynamicTitle( portletWindow, request );
593 }
594
595 response.setHeader( "JS_PORTLET_TITLE", StringEscapeUtils.escapeHtml( title ) );
596 dispatcher.getPortletContent(fragment).setTitle(title);
597 }
598 catch (Exception e)
599 {
600 log.error("Unable to reteive portlet title: " + e.getMessage(), e);
601 }
602 }
603 else
604 {
605 String title = null;
606
607 if (isCacheTitle)
608 {
609 title = fragment.getTitle();
610
611 if ( title == null )
612 {
613 title = addTitleService.getDynamicTitle(portletWindow, request);
614 }
615
616 dispatcher.getPortletContent(fragment).setTitle(title);
617 }
618
619 if (title == null)
620 {
621 title = addTitleService.getDynamicTitle(portletWindow, request);
622 dispatcher.getPortletContent(fragment).setTitle(title);
623 }
624 }
625 }
626
627 protected boolean checkSecurityConstraint(PortletDefinitionComposite portlet, ContentFragment fragment)
628 {
629 if (fragment.getType().equals(ContentFragment.PORTLET))
630 {
631 if (accessController != null)
632 {
633 return accessController.checkPortletAccess(portlet, JetspeedActions.MASK_VIEW);
634 }
635 }
636 return true;
637 }
638
639 protected void addToCache(PortletContent content)
640 {
641 CacheElement cachedElement = portletContentCache.createElement(content.getCacheKey(), content);
642 if (content.getExpiration() == -1)
643 {
644 cachedElement.setTimeToIdleSeconds(portletContentCache.getTimeToIdleSeconds());
645 cachedElement.setTimeToLiveSeconds(portletContentCache.getTimeToLiveSeconds());
646 }
647 else
648 {
649 cachedElement.setTimeToIdleSeconds(content.getExpiration());
650 cachedElement.setTimeToLiveSeconds(content.getExpiration());
651 }
652 portletContentCache.put(cachedElement);
653 }
654
655 public void notifyContentComplete(PortletContent content)
656 {
657 if (content.getExpiration() != 0)
658 addToCache(content);
659 }
660
661 public PortletTrackingManager getPortletTrackingManager()
662 {
663 return this.portletTracking;
664 }
665 }