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 + "BridgeClassName";
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.equals(PortletMode.EDIT) || mode.equals(PortletMode.HELP) || mode.equals(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].trim());
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 init
316       * parameter: javax.portlet.faces.DefaultContentType. If it doesn't exist or the value isn't in the 
317       * lisst of requested types, the portlet
318       * request's preferred response content type is returned.
319       * 
320       * Note:  This support is specific to the Portlet 1.0 Bridge.  Its value is
321       * likely to be ignored by the Portlet 2.0 Bridge or later.
322       * 
323       * @return the content type that should be used for this response.
324       */
325      public String getResponseContentType(PortletRequest request)
326      {
327        String contentType = 
328          getPortletConfig().getInitParameter(DEFAULT_CONTENT_TYPE);
329    
330        if (contentType == null || !isInRequestedContentTypes(request, contentType))
331        {
332          contentType = request.getResponseContentType();
333        }
334        return contentType;
335      }
336      
337      private boolean isInRequestedContentTypes(PortletRequest request, String contentTypeToCheck)
338      {
339        Enumeration e = request.getResponseContentTypes();
340        while (e.hasMoreElements()) 
341        {
342          if (contentTypeToCheck.equalsIgnoreCase((String) e.nextElement()))
343          {
344            return true;
345          }
346        }
347        return false;
348      }
349    
350      /**
351       * Returns the character set encoding used for this portlet response. Subclasses override to
352       * alter the default behavior. Default implementation returns value of the portlet init
353       * parameter: javax.portlet.faces.DefaultCharacterSetEncoding. If it doesn't exist null
354       * is returned.
355       * 
356       * Note:  This support is specific to the Portlet 1.0 Bridge.  Its value is
357       * likely to be ignored by the Portlet 2.0 Bridge or later.
358       * 
359       * @return the content type that should be used for this response.
360       */
361      public String getResponseCharacterSetEncoding(PortletRequest request)
362      {
363        return getPortletConfig().getInitParameter(DEFAULT_CHARACTERSET_ENCODING);
364      }
365    
366    
367      /**
368       * Returns the defaultViewIdMap the bridge should use when its unable to resolve to a specific
369       * target in the incoming request. There is one entry per support <code>PortletMode
370       * </code>.  The entry key is the name of the mode.  The entry value is the default viewId
371       * for that mode.
372       * 
373       * @return the defaultViewIdMap
374       */
375      public Map getDefaultViewIdMap()
376      {
377        if (mDefaultViewIdMap == null)
378        {
379          mDefaultViewIdMap = new HashMap<String, String>();
380          // loop through all portlet initialization parameters looking for those in the
381          // correct form
382          PortletConfig config = getPortletConfig();
383    
384          Enumeration<String> e = config.getInitParameterNames();
385          int len = DEFAULT_VIEWID.length();
386          while (e.hasMoreElements())
387          {
388            String s = e.nextElement();
389            if (s.startsWith(DEFAULT_VIEWID) && s.length() > DEFAULT_VIEWID.length())
390            {
391              String viewId = config.getInitParameter(s);
392              
393              // Don't add if there isn't a view
394              if (viewId == null || viewId.length() == 0) continue;
395              
396              // extract the mode
397              s = s.substring(len + 1);
398              mDefaultViewIdMap.put(s, viewId);
399            }
400          }
401        }
402    
403        return mDefaultViewIdMap;
404      }
405      
406      /**
407       * Returns an initialized bridge instance adequately prepared so the caller can
408       * call doFacesRequest directly without further initialization.
409       * 
410       * @return instance of the bridge.
411       * @throws PortletException exception acquiring or initializting the bridge.
412       */
413      public Bridge getFacesBridge(PortletRequest request, 
414                                   PortletResponse response) throws PortletException
415      {
416        initBridgeRequest(request, response);
417        return mFacesBridge;
418      }  
419      
420      private boolean isNonFacesRequest(PortletRequest request, PortletResponse response)
421      {
422        // Non Faces request is identified by either the presence of the _jsfBridgeNonFacesView
423        // parameter or the request being for a portlet mode which doesn't have a default
424        // Faces view configured for it.
425        if (request.getParameter(Bridge.NONFACES_TARGET_PATH_PARAMETER) != null)
426        {
427          return true;
428        }
429    
430        String modeDefaultViewId = mDefaultViewIdMap.get(request.getPortletMode().toString());
431        return modeDefaultViewId == null;
432      }
433    
434      private void doActionDispatchInternal(ActionRequest request, 
435                                            ActionResponse response) throws PortletException, 
436                                                                            IOException
437      {
438        // First determine whether this is a Faces or nonFaces request
439        if (isNonFacesRequest(request, response))
440        {
441          throw new PortletException("GenericFacesPortlet:  Action request is not for a Faces target.  Such nonFaces requests must be handled by a subclass.");
442        } else
443        {
444          doBridgeDispatch(request, response);
445        }
446      }
447    
448      private boolean doRenderDispatchInternal(RenderRequest request, 
449                                               RenderResponse response) throws PortletException, 
450                                                                               IOException
451      {
452        // First determine whether this is a Faces or nonFaces request
453        if (isNonFacesRequest(request, response))
454        {
455          return doNonFacesDispatch(request, response);
456        } else
457        {
458          WindowState state = request.getWindowState();
459          if (!state.equals(WindowState.MINIMIZED))
460          {
461            doBridgeDispatch(request, response);
462          }
463          return true;
464        }
465      }
466    
467      private boolean doNonFacesDispatch(RenderRequest request, 
468                                         RenderResponse response) throws PortletException
469      {
470        // Can only dispatch if the path is encoded in the request parameter
471        String targetPath = request.getParameter(Bridge.NONFACES_TARGET_PATH_PARAMETER);
472        if (targetPath == null)
473        {
474          // Didn't handle this request
475          return false;
476        }
477    
478        // merely dispatch this to the nonJSF target
479        // but because this is portlet 1.0 we have to ensure the content type is set.
480        // Ensure the ContentType is set before rendering
481        if (response.getContentType() == null)
482        {
483          response.setContentType(request.getResponseContentType());
484        }
485        try
486        {
487          PortletRequestDispatcher dispatcher = 
488            this.getPortletContext().getRequestDispatcher(targetPath);
489          dispatcher.include(request, response);
490          return true;
491        } catch (Exception e)
492        {
493          throw new PortletException("Unable to dispatch to: " + targetPath, e);
494        }
495      }
496    
497      private void doBridgeDispatch(RenderRequest request, 
498                                    RenderResponse response) throws PortletException
499      {
500        // Set the response ContentType/CharacterSet
501        setResponseContentType(response, getResponseContentType(request), 
502                               getResponseCharacterSetEncoding(request));
503    
504        try
505        {
506          getFacesBridge(request, response).doFacesRequest(request, response);
507        } catch (BridgeException e)
508        {
509          throw new PortletException("doBridgeDispatch failed:  error from Bridge in executing the request", 
510                                     e);
511        }
512    
513      }
514    
515      private void doBridgeDispatch(ActionRequest request, 
516                                    ActionResponse response) throws PortletException
517      {
518    
519        try
520        {
521          getFacesBridge(request, response).doFacesRequest(request, response);
522        } catch (BridgeException e)
523        {
524          throw new PortletException("doBridgeDispatch failed:  error from Bridge in executing the request", 
525                                     e);
526        }
527    
528      }
529    
530      private void initBridgeRequest(PortletRequest request, 
531                                     PortletResponse response) throws PortletException
532      {
533        initBridge();
534    
535    
536        // Now do any per request initialization
537        // I nthis case look to see if the request is encoded (usually 
538        // from a NonFaces view response) with the specific Faces
539        // view to execute.
540        String view = request.getParameter(Bridge.FACES_VIEW_ID_PARAMETER);
541        if (view != null)
542        {
543          request.setAttribute(Bridge.VIEW_ID, view);
544        } else
545        {
546          view = request.getParameter(Bridge.FACES_VIEW_PATH_PARAMETER);
547          if (view != null)
548          {
549            request.setAttribute(Bridge.VIEW_PATH, view);
550          }
551        }
552      }
553    
554      private void initBridge() throws PortletException
555      {
556        // Ensure te Bridge has been constrcuted and initialized
557        if (mFacesBridge == null)
558        {
559          try
560          {
561            // ensure we only ever create/init one bridge per portlet
562            synchronized(mLock)
563            {
564              if (mFacesBridge == null)
565              {
566                mFacesBridge = mFacesBridgeClass.newInstance();
567                mFacesBridge.init(getPortletConfig());
568              }
569            }
570          }
571          catch (Exception e)
572          {
573            throw new PortletException("doBridgeDisptach:  error instantiating the bridge class", e);
574          }
575        }
576      }
577    
578      private void setResponseContentType(RenderResponse response, String contentType, 
579                                          String charSetEncoding)
580      {
581        if (contentType == null)
582        {
583          return;
584    
585        }
586        if (charSetEncoding != null)
587        {
588          StringBuffer buf = new StringBuffer(contentType);
589          buf.append(";");
590          buf.append(charSetEncoding);
591          response.setContentType(buf.toString());
592        } else
593        {
594          response.setContentType(contentType);
595        }
596      }
597    
598      private String getFromServicesPath(PortletContext context, String resourceName)
599      {
600        // Check for a services definition
601        String result = null;
602        BufferedReader reader = null;
603        InputStream stream = null;
604        try
605        {
606          ClassLoader cl = Thread.currentThread().getContextClassLoader();
607          if (cl == null)
608          {
609            return null;
610          }
611    
612          stream = cl.getResourceAsStream(resourceName);
613          if (stream != null)
614          {
615            // Deal with systems whose native encoding is possibly
616            // different from the way that the services entry was created
617            try
618            {
619              reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
620            } catch (UnsupportedEncodingException e)
621            {
622              reader = new BufferedReader(new InputStreamReader(stream));
623            }
624            result = reader.readLine();
625            if (result != null)
626            {
627              result = result.trim();
628            }
629            reader.close();
630            reader = null;
631            stream = null;
632          }
633        } catch (IOException e)
634        {
635        } catch (SecurityException e)
636        {
637        } finally
638        {
639          if (reader != null)
640          {
641            try
642            {
643              reader.close();
644              stream = null;
645            } catch (Throwable t)
646            {
647              ;
648            }
649            reader = null;
650          }
651          if (stream != null)
652          {
653            try
654            {
655              stream.close();
656            } catch (Throwable t)
657            {
658              ;
659            }
660            stream = null;
661          }
662        }
663        return result;
664      }
665    
666    }