001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
003     * agreements. See the NOTICE file distributed with this work for additional information regarding
004     * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
005     * "License"); you may not use this file except in compliance with the License. You may obtain a
006     * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable
007     * law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
008     * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
009     * for the specific language governing permissions and limitations under the License.
010     */
011    package javax.portlet.faces;
012    
013    import java.io.BufferedReader;
014    import java.io.IOException;
015    import java.io.InputStream;
016    import java.io.InputStreamReader;
017    import java.io.UnsupportedEncodingException;
018    
019    import java.util.ArrayList;
020    import java.util.Enumeration;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Map;
024    
025    import javax.portlet.ActionRequest;
026    import javax.portlet.ActionResponse;
027    import javax.portlet.GenericPortlet;
028    import javax.portlet.PortletConfig;
029    import javax.portlet.PortletContext;
030    import javax.portlet.PortletException;
031    import javax.portlet.PortletMode;
032    import javax.portlet.PortletRequest;
033    import javax.portlet.PortletRequestDispatcher;
034    import javax.portlet.PortletResponse;
035    import javax.portlet.RenderRequest;
036    import javax.portlet.RenderResponse;
037    import javax.portlet.WindowState;
038    
039    /**
040     * The <code>GenericFacesPortlet</code> is provided to simplify development of a portlet that in
041     * whole or part relies on the Faces bridge to process requests. If all requests are to be handled
042     * by the bridge, <code>GenericFacesPortlet</code> is a turnkey implementation. Developers do not
043     * need to subclass it. However, if there are some situations where the portlet doesn't require
044     * bridge services then <code>GenericFacesPortlet</code> can be subclassed and overriden.
045     * <p>
046     * Since <code>GenericFacesPortlet</code> subclasses <code>GenericPortlet</code> care is taken
047     * to all subclasses to override naturally. For example, though <code>doDispatch()</code> is
048     * overriden, requests are only dispatched to the bridge from here if the <code>PortletMode</code>
049     * isn't <code>VIEW</code>, <code>EDIT</code>, or <code>HELP</code>.
050     * <p>
051     * The <code>GenericFacesPortlet</code> recognizes the following portlet initialization
052     * parameters:
053     * <ul>
054     * <li><code>javax.portlet.faces.defaultViewId.[<i>mode</i>]</code>: specifies on a per mode
055     * basis the default viewId the Bridge executes when not already encoded in the incoming request. A
056     * value must be defined for each <code>PortletMode</code> the <code>Bridge</code> is expected
057     * to process. </li>
058     *  <li><code>javax.portlet.faces.excludedRequestAttributes</code>: specifies on a per portlet
059     * basis the set of request attributes the bridge is to exclude from its request scope.  The
060     * value of this parameter is a comma delimited list of either fully qualified attribute names or
061     * a partial attribute name of the form <i>packageName.*</i>.  In this later case all attributes
062     * exactly prefixed by <i>packageName</i> are excluded, non recursive.</li>
063     *  <li><code>javax.portlet.faces.preserveActionParams</code>: specifies on a per portlet
064     * basis whether the bridge should preserve parameters received in an action request
065     * and restore them for use during subsequent renders.</li>
066     *  <li><code>javax.portlet.faces.defaultContentType</code>: specifies on a per mode
067     * basis the content type the bridge should set for all render requests it processes. </li>
068     *  <li><code>javax.portlet.faces.defaultCharacterSetEncoding</code>: specifies on a per mode
069     * basis the default character set encoding the bridge should set for all render requests it
070     * processes</li>
071     * </ul>
072     * The <code>GenericFacesPortlet</code> recognizes the following application
073     * (<code>PortletContext</code>) initialization parameters:
074     * <ul>
075     * <li><code>javax.portlet.faces.BridgeImplClass</code>: specifies the <code>Bridge</code>implementation
076     * class used by this portlet. Typically this initialization parameter isn't set as the 
077     * <code>GenericFacesPortlet</code> defaults to finding the class name from the bridge
078     * configuration.  However if more then one bridge is configured in the environment such 
079     * per application configuration is necessary to force a specific bridge to be used.
080     * </li>
081     * </ul>
082     */
083    public class GenericFacesPortlet extends GenericPortlet
084    {
085      /** Application (PortletContext) init parameter that names the bridge class used
086       * by this application.  Typically not used unless more then 1 bridge is configured
087       * in an environment as its more usual to rely on the self detection.
088       */
089      public static final String BRIDGE_CLASS = Bridge.BRIDGE_PACKAGE_PREFIX + "BridgeImplClass";
090    
091      /** Portlet init parameter that defines the default ViewId that should be used
092       * when the request doesn't otherwise convery the target.  There must be one 
093       * initialization parameter for each supported mode.  Each parameter is named
094       * DEFAULT_VIEWID.<i>mode</i>, where <i>mode</i> is the name of the corresponding
095       * <code>PortletMode</code>
096       */
097      public static final String DEFAULT_VIEWID = Bridge.BRIDGE_PACKAGE_PREFIX + "defaultViewId";
098    
099      /** Portlet init parameter that defines the render response ContentType the bridge 
100       * sets prior to rendering.  If not set the bridge uses the request's preferred
101       * content type.
102       */
103      public static final String DEFAULT_CONTENT_TYPE = 
104        Bridge.BRIDGE_PACKAGE_PREFIX + "defaultContentType";
105    
106      /** Portlet init parameter that defines the render response CharacterSetEncoding the bridge 
107       * sets prior to rendering.  Typcially only set when the jsp outputs an encoding other
108       * then the portlet container's and the portlet container supports response encoding
109       * transformation.
110       */
111      public static final String DEFAULT_CHARACTERSET_ENCODING = 
112        Bridge.BRIDGE_PACKAGE_PREFIX + "defaultCharacterSetEncoding";
113    
114      /** Location of the services descriptor file in a brige installation that defines 
115       * the class name of the bridge implementation.
116       */
117      public static final String BRIDGE_SERVICE_CLASSPATH = 
118        "META-INF/services/javax.portlet.faces.Bridge";
119    
120      private Class<? extends Bridge> mFacesBridgeClass = null;
121      private Bridge mFacesBridge = null;
122      private HashMap<String, String> mDefaultViewIdMap = null;
123      private Object mLock = new Object();  // used to synchronize on when initializing the bridge.
124    
125      /**
126       * Initialize generic faces portlet from portlet.xml
127       */
128      @SuppressWarnings("unchecked")
129      @Override
130      public void init(PortletConfig portletConfig) throws PortletException
131      {
132        super.init(portletConfig);
133    
134        // Make sure the bridge impl class is defined -- if not then search for it
135        // using same search rules as Faces
136        String bridgeClassName = getBridgeClassName();
137    
138        if (bridgeClassName != null)
139        {
140          try
141          {
142            ClassLoader loader = Thread.currentThread().getContextClassLoader();
143            mFacesBridgeClass = (Class<? extends Bridge>) loader.loadClass(bridgeClassName);
144          } catch (ClassNotFoundException cnfe)
145          {
146            throw new PortletException("Unable to load configured bridge class: " + bridgeClassName);
147          }
148        }
149        else
150        {
151          throw new PortletException("Can't locate configuration parameter defining the bridge class to use for this portlet:" + getPortletName());
152        }
153    
154        // Get the other bridge configuration parameters and set as context attributes
155        List<String> excludedAttrs = getExcludedRequestAttributes();
156        if (excludedAttrs != null)
157        {
158          getPortletContext().setAttribute(Bridge.BRIDGE_PACKAGE_PREFIX + getPortletName() + "." + 
159                                           Bridge.EXCLUDED_REQUEST_ATTRIBUTES, excludedAttrs);
160        }
161    
162        Boolean preserveActionParams = new Boolean(isPreserveActionParameters());
163        getPortletContext().setAttribute(Bridge.BRIDGE_PACKAGE_PREFIX + getPortletName() + "." + 
164                                         Bridge.PRESERVE_ACTION_PARAMS, preserveActionParams);
165    
166        Map defaultViewIdMap = getDefaultViewIdMap();
167        getPortletContext().setAttribute(Bridge.BRIDGE_PACKAGE_PREFIX + getPortletName() + "." + 
168                                         Bridge.DEFAULT_VIEWID_MAP, defaultViewIdMap);
169    
170        // Don't instanciate/initialize the bridge yet. Do it on first use
171      }
172    
173      /**
174       * Release resources, specifically it destroys the bridge.
175       */
176      @Override
177      public void destroy()
178      {
179        if (mFacesBridge != null)
180        {
181          mFacesBridge.destroy();
182          mFacesBridge = null;
183          mFacesBridgeClass = null;
184        }
185        mDefaultViewIdMap = null;
186        
187        super.destroy();
188      }
189    
190      /**
191       * If mode is VIEW, EDIT, or HELP -- defer to the doView, doEdit, doHelp so subclasses can
192       * override. Otherwise handle mode here if there is a defaultViewId mapping for it.
193       */
194      @Override
195      public void doDispatch(RenderRequest request, RenderResponse response) throws PortletException, 
196                                                                                    IOException
197      {
198        // Defer to helper methods for standard modes so subclasses can override
199        PortletMode mode = request.getPortletMode();
200        if (mode == PortletMode.EDIT || mode == PortletMode.HELP || mode == PortletMode.VIEW)
201        {
202          super.doDispatch(request, response);
203        } else
204        {
205          // Bridge didn't process this one -- so forge ahead
206          if (!doRenderDispatchInternal(request, response))
207          {
208            super.doDispatch(request, response);
209          }
210        }
211      }
212    
213      @Override
214      protected void doEdit(RenderRequest request, RenderResponse response) throws PortletException, 
215                                                                                   java.io.IOException
216      {
217        doRenderDispatchInternal(request, response);
218      }
219    
220      @Override
221      protected void doHelp(RenderRequest request, RenderResponse response) throws PortletException, 
222                                                                                   java.io.IOException
223      {
224        doRenderDispatchInternal(request, response);
225      }
226    
227      @Override
228      protected void doView(RenderRequest request, RenderResponse response) throws PortletException, 
229                                                                                   java.io.IOException
230      {
231        doRenderDispatchInternal(request, response);
232      }
233    
234      @Override
235      public void processAction(ActionRequest request, 
236                                ActionResponse response) throws PortletException, IOException
237      {
238        doActionDispatchInternal(request, response);
239      }
240    
241      /**
242       * Returns the set of RequestAttribute names that the portlet wants the bridge to
243       * exclude from its managed request scope.  This default implementation picks up
244       * this list from the comma delimited init_param javax.portlet.faces.excludedRequestAttributes.
245       * 
246       * @return a List containing the names of the attributes to be excluded. null if it can't be
247       *         determined.
248       */
249      public List<String> getExcludedRequestAttributes()
250      {
251        String excludedAttrs = 
252          getPortletConfig().getInitParameter(Bridge.BRIDGE_PACKAGE_PREFIX + Bridge.EXCLUDED_REQUEST_ATTRIBUTES);
253        if (excludedAttrs == null)
254        {
255          return null;
256        }
257    
258        String[] attrArray = excludedAttrs.split(",");
259        // process comma delimited String into a List
260        ArrayList<String> list = new ArrayList(attrArray.length);
261        for (int i = 0; i < attrArray.length; i++)
262        {
263          list.add(attrArray[i]);
264        }
265        return list;
266      }
267    
268      /**
269       * Returns a boolean indicating whether or not the bridge should preserve all the
270       * action parameters in the subsequent renders that occur in the same scope.  This
271       * default implementation reads the values from the portlet init_param
272       * javax.portlet.faces.preserveActionParams.  If not present, false is returned.
273       * 
274       * @return a boolean indicating whether or not the bridge should preserve all the
275       * action parameters in the subsequent renders that occur in the same scope.
276       */
277      public boolean isPreserveActionParameters()
278      {
279        String preserveActionParams = 
280          getPortletConfig().getInitParameter(Bridge.BRIDGE_PACKAGE_PREFIX + 
281                                              Bridge.PRESERVE_ACTION_PARAMS);
282        if (preserveActionParams == null)
283        {
284          return false;
285        } else
286        {
287          return Boolean.parseBoolean(preserveActionParams);
288        }
289      }
290    
291      /**
292       * Returns the className of the bridge implementation this portlet uses. Subclasses override to
293       * alter the default behavior. Default implementation first checks for a portlet context init
294       * parameter: javax.portlet.faces.BridgeImplClass. If it doesn't exist then it looks for the
295       * resource file "META-INF/services/javax.portlet.faces.Bridge" using the current threads
296       * classloader and extracts the classname from the first line in that file.
297       * 
298       * @return the class name of the Bridge class the GenericFacesPortlet uses. null if it can't be
299       *         determined.
300       */
301      public String getBridgeClassName()
302      {
303        String bridgeClassName = getPortletConfig().getPortletContext().getInitParameter(BRIDGE_CLASS);
304    
305        if (bridgeClassName == null)
306        {
307          bridgeClassName = 
308              getFromServicesPath(getPortletConfig().getPortletContext(), BRIDGE_SERVICE_CLASSPATH);
309        }
310        return bridgeClassName;
311      }
312    
313      /**
314       * Returns the default content type for this portlet request. Subclasses override to
315       * alter the default behavior. Default implementation returns value of the portlet context init
316       * parameter: javax.portlet.faces.DefaultContentType. If it doesn't exist the portlet
317       * request's preferred response content type is returned.
318       * 
319       * Note:  This support is specific to the Portlet 1.0 Bridge.  Its value is
320       * likely to be ignored by the Portlet 2.0 Bridge or later.
321       * 
322       * @return the content type that should be used for this response.
323       */
324      public String getResponseContentType(PortletRequest request)
325      {
326        String contentType = 
327          getPortletConfig().getPortletContext().getInitParameter(DEFAULT_CONTENT_TYPE);
328    
329        if (contentType == null || !isInRequestedContentTypes(request, contentType))
330        {
331          contentType = request.getResponseContentType();
332        }
333        return contentType;
334      }
335      
336      private boolean isInRequestedContentTypes(PortletRequest request, String contentTypeToCheck)
337      {
338        Enumeration e = request.getResponseContentTypes();
339        while (e.hasMoreElements()) 
340        {
341          if (contentTypeToCheck.equalsIgnoreCase((String) e.nextElement()))
342          {
343            return true;
344          }
345        }
346        return false;
347      }
348    
349      /**
350       * Returns the character set encoding used for this portlet response. Subclasses override to
351       * alter the default behavior. Default implementation returns value of the portlet context init
352       * parameter: javax.portlet.faces.DefaultCharacterSetEncoding. If it doesn't exist null
353       * is returned.
354       * 
355       * Note:  This support is specific to the Portlet 1.0 Bridge.  Its value is
356       * likely to be ignored by the Portlet 2.0 Bridge or later.
357       * 
358       * @return the content type that should be used for this response.
359       */
360      public String getResponseCharacterSetEncoding(PortletRequest request)
361      {
362        return getPortletConfig().getPortletContext().getInitParameter(DEFAULT_CHARACTERSET_ENCODING);
363      }
364    
365    
366      /**
367       * Returns the defaultViewIdMap the bridge should use when its unable to resolve to a specific
368       * target in the incoming request. There is one entry per support <code>PortletMode
369       * </code>.  The entry key is the name of the mode.  The entry value is the default viewId
370       * for that mode.
371       * 
372       * @return the defaultViewIdMap
373       */
374      public Map getDefaultViewIdMap()
375      {
376        if (mDefaultViewIdMap == null)
377        {
378          mDefaultViewIdMap = new HashMap<String, String>();
379          // loop through all portlet initialization parameters looking for those in the
380          // correct form
381          PortletConfig config = getPortletConfig();
382    
383          Enumeration<String> e = config.getInitParameterNames();
384          int len = DEFAULT_VIEWID.length();
385          while (e.hasMoreElements())
386          {
387            String s = e.nextElement();
388            if (s.startsWith(DEFAULT_VIEWID) && s.length() > DEFAULT_VIEWID.length())
389            {
390              String viewId = config.getInitParameter(s);
391              // extract the mode
392              s = s.substring(len + 1);
393              mDefaultViewIdMap.put(s, viewId);
394            }
395          }
396        }
397    
398        return mDefaultViewIdMap;
399      }
400      
401      /**
402       * Returns an initialized bridge instance adequately prepared so the caller can
403       * call doFacesRequest directly without further initialization.
404       * 
405       * @return instance of the bridge.
406       * @throws PortletException exception acquiring or initializting the bridge.
407       */
408      public Bridge getFacesBridge(PortletRequest request, 
409                                   PortletResponse response) throws PortletException
410      {
411        initBridgeRequest(request, response);
412        return mFacesBridge;
413      }  
414      
415      private boolean isNonFacesRequest(PortletRequest request, PortletResponse response)
416      {
417        // Non Faces request is identified by either the presence of the _jsfBridgeNonFacesView
418        // parameter or the request being for a portlet mode which doesn't have a default
419        // Faces view configured for it.
420        if (request.getParameter(Bridge.NONFACES_TARGET_PATH_PARAMETER) != null)
421        {
422          return true;
423        }
424    
425        String modeDefaultViewId = mDefaultViewIdMap.get(request.getPortletMode().toString());
426        return modeDefaultViewId == null;
427      }
428    
429      private void doActionDispatchInternal(ActionRequest request, 
430                                            ActionResponse response) throws PortletException, 
431                                                                            IOException
432      {
433        // First determine whether this is a Faces or nonFaces request
434        if (isNonFacesRequest(request, response))
435        {
436          throw new PortletException("GenericFacesPortlet:  Action request is not for a Faces target.  Such nonFaces requests must be handled by a subclass.");
437        } else
438        {
439          doBridgeDispatch(request, response);
440        }
441      }
442    
443      private boolean doRenderDispatchInternal(RenderRequest request, 
444                                               RenderResponse response) throws PortletException, 
445                                                                               IOException
446      {
447        // First determine whether this is a Faces or nonFaces request
448        if (isNonFacesRequest(request, response))
449        {
450          return doNonFacesDispatch(request, response);
451        } else
452        {
453          WindowState state = request.getWindowState();
454          if (!state.equals(WindowState.MINIMIZED))
455          {
456            doBridgeDispatch(request, response);
457          }
458          return true;
459        }
460      }
461    
462      private boolean doNonFacesDispatch(RenderRequest request, 
463                                         RenderResponse response) throws PortletException
464      {
465        // Can only dispatch if the path is encoded in the request parameter
466        String targetPath = request.getParameter(Bridge.NONFACES_TARGET_PATH_PARAMETER);
467        if (targetPath == null)
468        {
469          // Didn't handle this request
470          return false;
471        }
472    
473        // merely dispatch this to the nonJSF target
474        // but because this is portlet 1.0 we have to ensure the content type is set.
475        // Ensure the ContentType is set before rendering
476        if (response.getContentType() == null)
477        {
478          response.setContentType(request.getResponseContentType());
479        }
480        try
481        {
482          PortletRequestDispatcher dispatcher = 
483            this.getPortletContext().getRequestDispatcher(targetPath);
484          dispatcher.include(request, response);
485          return true;
486        } catch (Exception e)
487        {
488          throw new PortletException("Unable to dispatch to: " + targetPath, e);
489        }
490      }
491    
492      private void doBridgeDispatch(RenderRequest request, 
493                                    RenderResponse response) throws PortletException
494      {
495        // Set the response ContentType/CharacterSet
496        setResponseContentType(response, getResponseContentType(request), 
497                               getResponseCharacterSetEncoding(request));
498    
499        try
500        {
501          getFacesBridge(request, response).doFacesRequest(request, response);
502        } catch (BridgeException e)
503        {
504          throw new PortletException("doBridgeDispatch failed:  error from Bridge in executing the request", 
505                                     e);
506        }
507    
508      }
509    
510      private void doBridgeDispatch(ActionRequest request, 
511                                    ActionResponse response) throws PortletException
512      {
513    
514        try
515        {
516          getFacesBridge(request, response).doFacesRequest(request, response);
517        } catch (BridgeException e)
518        {
519          throw new PortletException("doBridgeDispatch failed:  error from Bridge in executing the request", 
520                                     e);
521        }
522    
523      }
524    
525      private void initBridgeRequest(PortletRequest request, 
526                                     PortletResponse response) throws PortletException
527      {
528        initBridge();
529    
530    
531        // Now do any per request initialization
532        // I nthis case look to see if the request is encoded (usually 
533        // from a NonFaces view response) with the specific Faces
534        // view to execute.
535        String view = request.getParameter(Bridge.FACES_VIEW_ID_PARAMETER);
536        if (view != null)
537        {
538          request.setAttribute(Bridge.VIEW_ID, view);
539        } else
540        {
541          view = request.getParameter(Bridge.FACES_VIEW_PATH_PARAMETER);
542          if (view != null)
543          {
544            request.setAttribute(Bridge.VIEW_PATH, view);
545          }
546        }
547      }
548    
549      private void initBridge() throws PortletException
550      {
551        // Ensure te Bridge has been constrcuted and initialized
552        if (mFacesBridge == null)
553        {
554          try
555          {
556            // ensure we only ever create/init one bridge per portlet
557            synchronized(mLock)
558            {
559              if (mFacesBridge == null)
560              {
561                mFacesBridge = mFacesBridgeClass.newInstance();
562                mFacesBridge.init(getPortletConfig());
563              }
564            }
565          }
566          catch (Exception e)
567          {
568            throw new PortletException("doBridgeDisptach:  error instantiating the bridge class", e);
569          }
570        }
571      }
572    
573      private void setResponseContentType(RenderResponse response, String contentType, 
574                                          String charSetEncoding)
575      {
576        if (contentType == null)
577        {
578          return;
579    
580        }
581        if (charSetEncoding != null)
582        {
583          StringBuffer buf = new StringBuffer(contentType);
584          buf.append(";");
585          buf.append(charSetEncoding);
586          response.setContentType(buf.toString());
587        } else
588        {
589          response.setContentType(contentType);
590        }
591      }
592    
593      private String getFromServicesPath(PortletContext context, String resourceName)
594      {
595        // Check for a services definition
596        String result = null;
597        BufferedReader reader = null;
598        InputStream stream = null;
599        try
600        {
601          ClassLoader cl = Thread.currentThread().getContextClassLoader();
602          if (cl == null)
603          {
604            return null;
605          }
606    
607          stream = cl.getResourceAsStream(resourceName);
608          if (stream != null)
609          {
610            // Deal with systems whose native encoding is possibly
611            // different from the way that the services entry was created
612            try
613            {
614              reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
615            } catch (UnsupportedEncodingException e)
616            {
617              reader = new BufferedReader(new InputStreamReader(stream));
618            }
619            result = reader.readLine();
620            if (result != null)
621            {
622              result = result.trim();
623            }
624            reader.close();
625            reader = null;
626            stream = null;
627          }
628        } catch (IOException e)
629        {
630        } catch (SecurityException e)
631        {
632        } finally
633        {
634          if (reader != null)
635          {
636            try
637            {
638              reader.close();
639              stream = null;
640            } catch (Throwable t)
641            {
642              ;
643            }
644            reader = null;
645          }
646          if (stream != null)
647          {
648            try
649            {
650              stream.close();
651            } catch (Throwable t)
652            {
653              ;
654            }
655            stream = null;
656          }
657        }
658        return result;
659      }
660    
661    }