1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. 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,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.myfaces.trinidadinternal.application;
20
21 import java.io.File;
22 import java.io.InputStream;
23 import java.io.IOException;
24 import java.lang.reflect.Constructor;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Enumeration;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.Properties;
34
35 import javax.faces.FacesException;
36 import javax.faces.application.ViewHandler;
37 import javax.faces.application.ViewHandlerWrapper;
38 import javax.faces.component.UIViewRoot;
39 import javax.faces.context.ExternalContext;
40 import javax.faces.context.FacesContext;
41
42 import org.apache.myfaces.trinidad.context.RequestContext;
43 import org.apache.myfaces.trinidad.logging.TrinidadLogger;
44 import org.apache.myfaces.trinidad.render.ExtendedRenderKitService;
45 import org.apache.myfaces.trinidad.render.InternalView;
46 import org.apache.myfaces.trinidad.util.Service;
47 import org.apache.myfaces.trinidadinternal.context.RequestContextImpl;
48 import org.apache.myfaces.trinidadinternal.context.TrinidadPhaseListener;
49 import org.apache.myfaces.trinidadinternal.share.config.Configuration;
50 import org.apache.myfaces.trinidadinternal.util.URLUtils;
51
52 /**
53 * ViewHandler that adds modification detection to the existing ViewHandler,
54 * assuming that the viewId is a valid resource path.
55 * <p>
56 * And now also supports inserting URLs tokens to preserve PageFlowScope.
57 * <p>
58 * @version $Name: $ ($Revision: adfrt/faces/adf-faces-impl/src/main/java/oracle/adfinternal/view/faces/application/ViewHandlerImpl.java#0 $) $Date: 05-jan-2006.13:19:09 $
59 * @todo Rename something less generic
60 * @todo Support extension mapping (*.faces)
61 * @todo The modification detection only works for a single user. That's
62 * OK for now, because it's intended for use while developing
63 */
64 public class ViewHandlerImpl extends ViewHandlerWrapper
65 {
66 static public final String ALTERNATE_VIEW_HANDLER =
67 "org.apache.myfaces.trinidad.ALTERNATE_VIEW_HANDLER";
68
69 public ViewHandlerImpl(
70 ViewHandler delegate)
71 {
72 _delegate = delegate;
73 _timestamps = new HashMap<String, Long>();
74 _loadInternalViews();
75 }
76
77 protected ViewHandler getWrapped()
78 {
79 return _delegate;
80 }
81
82 @Override
83 public UIViewRoot createView(FacesContext context, String viewId)
84 {
85 _initIfNeeded(context);
86
87 InternalView internal = _getInternalView(context, viewId);
88 if (internal != null)
89 {
90 UIViewRoot root = internal.createView(context, viewId);
91 if (root != null)
92 return root;
93 // Otherwise, fall through to default processing
94 }
95 else if (_checkTimestamp(context))
96 {
97 try
98 {
99 // Check the timestamp on the physical path
100 String path = _getPath(viewId);
101 synchronized (_timestamps)
102 {
103 Long ts = _timestamps.get(path);
104 if (ts != _NOT_FOUND)
105 {
106 URL url = context.getExternalContext().getResource(path);
107 Long modified = _getLastModified(url);
108 _timestamps.put(path, modified);
109 }
110 }
111 }
112 catch (IOException e)
113 {
114 _LOG.severe(e);
115 }
116 }
117
118 return super.createView(context, viewId);
119 }
120
121 @Override
122 public String getActionURL(FacesContext context, String viewId)
123 {
124 String actionURL = super.getActionURL(context, viewId);
125 RequestContext afContext = RequestContext.getCurrentInstance();
126 if (afContext != null)
127 {
128 actionURL = afContext.getPageResolver().encodeActionURI(actionURL);
129 actionURL = afContext.getPageFlowScopeProvider().
130 encodeCurrentPageFlowScopeURL(context, actionURL);
131 }
132
133 return actionURL;
134 }
135
136 @Override
137 public String getResourceURL(
138 FacesContext context,
139 String path)
140 {
141 return super.getResourceURL(context, path);
142 }
143
144
145 @Override
146 public void renderView(
147 FacesContext context,
148 UIViewRoot viewToRender) throws IOException, FacesException
149 {
150 _initIfNeeded(context);
151
152 // See if there is a possiblity of short-circuiting the current
153 // Render Response
154 ExtendedRenderKitService service = _getExtendedRenderKitService(context);
155 if ((service != null) &&
156 service.shortCircuitRenderView(context))
157 {
158 // Yup, we don't need to do anything
159 ;
160 }
161 else
162 {
163 try
164 {
165 if (service != null)
166 service.encodeBegin(context);
167
168 InternalView internal = _getInternalView(context,
169 viewToRender.getViewId());
170 if (internal != null)
171 {
172 internal.renderView(context, viewToRender);
173 }
174 else
175 {
176 super.renderView(context, viewToRender);
177 }
178
179 if (service != null)
180 service.encodeEnd(context);
181 }
182 finally
183 {
184 if (service != null)
185 service.encodeFinally(context);
186 }
187 }
188 }
189
190 @Override
191 public UIViewRoot restoreView(
192 FacesContext context,
193 String viewId)
194 {
195 // If we're being asked to re-run the lifecycle for a "return"
196 // event, always restore the "launch view", which was set
197 // over in RequestContextImpl
198 UIViewRoot launchView = (UIViewRoot)
199 context.getExternalContext().getRequestMap().get(
200 RequestContextImpl.LAUNCH_VIEW);
201
202 if (launchView != null)
203 {
204 context.getExternalContext().getRequestMap().remove(
205 RequestContextImpl.LAUNCH_VIEW);
206 TrinidadPhaseListener.markPostback(context);
207 return launchView;
208 }
209
210 InternalView internal = _getInternalView(context, viewId);
211 if (internal != null)
212 {
213 return internal.restoreView(context, viewId);
214 }
215
216 boolean uptodate = true;
217
218 if (_checkTimestamp(context))
219 {
220 try
221 {
222 // Check the timestamp on the physical path
223 String path = _getPath(viewId);
224 synchronized (_timestamps)
225 {
226 Long ts = _timestamps.get(path);
227 if (ts != _NOT_FOUND)
228 {
229 URL url = context.getExternalContext().getResource(path);
230 Long modified = _getLastModified(url);
231 if (modified == _NOT_FOUND)
232 {
233 _timestamps.put(path, _NOT_FOUND);
234 }
235 else if ((ts == null) ||
236 (modified.longValue() > ts.longValue()))
237 {
238 _timestamps.put(path, modified);
239 if (ts != null)
240 {
241 _LOG.fine("View document \"" + path + "\" has been modified, " +
242 "ignoring postback for view \"" + viewId +"\"");
243 }
244 uptodate = false;
245 }
246 }
247 }
248 }
249 catch (IOException e)
250 {
251 _LOG.severe(e);
252 }
253 }
254
255 if (!uptodate)
256 {
257 return null;
258 }
259
260 UIViewRoot result = super.restoreView(context, viewId);
261 // If we've successfully restored a view, then assume that
262 // this is a postback request.
263 if (result != null)
264 {
265 TrinidadPhaseListener.markPostback(context);
266 }
267
268 return result;
269 }
270
271 @Override
272 public void writeState(
273 FacesContext context) throws IOException
274 {
275 String viewId = context.getViewRoot().getViewId();
276 InternalView internal =
277 _getInternalView(context, viewId);
278
279 // As internal views whether they're stateless. If they are, don't
280 // bother writing anything out.
281 if ((internal != null) && internal.isStateless(context, viewId))
282 return;
283
284 ExtendedRenderKitService service = _getExtendedRenderKitService(context);
285 if ((service != null) &&
286 service.isStateless(context))
287 return;
288
289 super.writeState(context);
290 }
291
292 synchronized private void _initIfNeeded(FacesContext context)
293 {
294 if (!_inited)
295 {
296 _inited = true;
297 String alternateViewHandler =
298 context.getExternalContext().getInitParameter(ALTERNATE_VIEW_HANDLER);
299 if (alternateViewHandler != null)
300 {
301 ViewHandler viewHandlerInstance = null;
302 try
303 {
304 ClassLoader loader = Thread.currentThread().getContextClassLoader();
305 Class<?> c = loader.loadClass(alternateViewHandler);
306 try
307 {
308 Constructor<?> constructor = c.getConstructor(
309 new Class[]{ViewHandler.class});
310 viewHandlerInstance =
311 (ViewHandler) constructor.newInstance(new Object[]{_delegate});
312 }
313 catch (NoSuchMethodException nsme)
314 {
315 viewHandlerInstance = (ViewHandler) c.newInstance();
316 }
317 }
318 catch (Exception e)
319 {
320 _LOG.warning("CANNOT_LOAD_VIEWHANDLER", alternateViewHandler);
321 _LOG.warning(e);
322 }
323
324 if (viewHandlerInstance != null)
325 _delegate = viewHandlerInstance;
326 }
327 }
328 }
329
330 private ExtendedRenderKitService _getExtendedRenderKitService(
331 FacesContext context)
332 {
333 return Service.getService(context.getRenderKit(),
334 ExtendedRenderKitService.class);
335 }
336
337 private boolean _checkTimestamp(FacesContext context)
338 {
339 if (_checkTimestamp == null)
340 {
341 String checkTimestamp =
342 context.getExternalContext().getInitParameter(Configuration.CHECK_TIMESTAMP_PARAM);
343 // Detect when we're running inside of the JDeveloper embedded OC4J
344 // environment - and there, always use timestamp checking
345 // TODO: come up with a non-proprietary way of checking this?
346 boolean performCheck = "true".equals(checkTimestamp) ||
347 "development".equals(System.getProperty("oracle.application.environment"));
348 _checkTimestamp = Boolean.valueOf(performCheck);
349 if ("true".equals(checkTimestamp))
350 {
351 _LOG.info("TIMESTAMP_CHECKING_ENABLED_SHOULDNOT_IN_PRODUCTION",
352 Configuration.CHECK_TIMESTAMP_PARAM);
353 }
354 }
355
356 return _checkTimestamp.booleanValue();
357 }
358
359
360 /**
361 * Return the physical path of a particular URI
362 */
363 static private String _getPath(String uri)
364 {
365 RequestContext afc = RequestContext.getCurrentInstance();
366 if (afc != null)
367 {
368 return afc.getPageResolver().getPhysicalURI(uri);
369 }
370
371 // No RequestContext? Just return the URI
372 return uri;
373 }
374
375
376 private Long _getLastModified(URL url) throws IOException
377 {
378 if (url == null)
379 return _NOT_FOUND;
380
381 return Long.valueOf(URLUtils.getLastModified(url));
382 }
383
384
385 private InternalView _getInternalView(
386 FacesContext context,
387 String viewId)
388 {
389 InternalView internal = _internalViews.get(viewId);
390 if (internal == null)
391 {
392 // If we're using suffix-mapping, then any internal viewId will
393 // get affixed with ".jsp" or ".jspx"; try trimming that off
394 // if present
395 ExternalContext external = context.getExternalContext();
396
397 // Only bother when using suffix-mapping (path info will always
398 // be non-null for prefix-mapping)
399 if (external.getRequestPathInfo() == null)
400 {
401 String suffix = external.getInitParameter("javax.faces.DEFAULT_SUFFIX");
402 if (suffix == null)
403 suffix = ".jspx";
404
405 if (viewId.endsWith(suffix))
406 {
407 String viewIdWithoutSuffix = viewId.substring(
408 0, viewId.length() - suffix.length());
409 internal = _internalViews.get(viewIdWithoutSuffix);
410 }
411 }
412 }
413
414 return internal;
415 }
416
417 //
418 // Load the META-INF/org.apache.myfaces.trinidad.render.InternalView.properties
419 // files.
420 //
421 private void _loadInternalViews()
422 {
423 _internalViews = new HashMap<String, InternalView>();
424 List<URL> list = new ArrayList<URL>();
425 ClassLoader loader = _getClassLoader();
426 try
427 {
428 Enumeration<URL> en = loader.getResources(
429 "META-INF/org.apache.myfaces.trinidad.render.InternalView.properties");
430 while (en.hasMoreElements())
431 {
432 list.add(en.nextElement());
433 }
434
435 // And, for some temporary backwards compatibility, also load
436 // the incorrect properties without "render"
437 en = loader.getResources(
438 "META-INF/org.apache.myfaces.trinidad.InternalView.properties");
439 while (en.hasMoreElements())
440 {
441 list.add(en.nextElement());
442 }
443
444
445 // Reverse the list so it is in the proper order (most local
446 // entry "wins")
447 Collections.reverse(list);
448 }
449 catch (IOException ioe)
450 {
451 _LOG.severe(ioe);
452 }
453
454 for (URL url : list)
455 {
456 try
457 {
458 Properties properties = new Properties();
459 _LOG.fine("Loading internal views from {0}", url);
460 InputStream is = url.openStream();
461 try
462 {
463 properties.load(is);
464 }
465 finally
466 {
467 is.close();
468 }
469
470 for (Map.Entry<Object, Object> entry : properties.entrySet())
471 {
472 String name = (String) entry.getKey();
473 String className = (String) entry.getValue();
474 Class<?> clazz = loader.loadClass(className);
475 InternalView view = (InternalView) clazz.newInstance();
476 _internalViews.put(name, view);
477 }
478 }
479 catch (IllegalAccessException iae)
480 {
481 _LOG.severe("CANNOT_LOAD_URL", url);
482 _LOG.severe(iae);
483 }
484 catch (InstantiationException ie)
485 {
486 _LOG.severe("CANNOT_LOAD_URL", url);
487 _LOG.severe(ie);
488 }
489 catch (ClassNotFoundException cnfe)
490 {
491 _LOG.severe("CANNOT_LOAD_URL", url);
492 _LOG.severe(cnfe);
493 }
494 catch (IOException ioe)
495 {
496 _LOG.severe("CANNOT_LOAD_URL", url);
497 _LOG.severe(ioe);
498 }
499 }
500 }
501
502
503 static private ClassLoader _getClassLoader()
504 {
505 ClassLoader loader = Thread.currentThread().getContextClassLoader();
506 if (loader == null)
507 loader = ViewHandlerImpl.class.getClassLoader();
508 return loader;
509 }
510
511 private Boolean _checkTimestamp;
512 // Mostly final, but see _initIfNeeded()
513 private ViewHandler _delegate;
514 private final Map<String, Long> _timestamps;
515 private boolean _inited;
516 private Map<String, InternalView> _internalViews;
517
518 private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(ViewHandlerImpl.class);
519 private static final Long _NOT_FOUND = Long.valueOf(0);
520 }