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    
124      /**
125       * Initialize generic faces portlet from portlet.xml
126       */
127      @SuppressWarnings("unchecked")
128      @Override
129      public void init(PortletConfig portletConfig) throws PortletException
130      {
131        super.init(portletConfig);
132    
133        // Make sure the bridge impl class is defined -- if not then search for it
134        // using same search rules as Faces
135        String bridgeClassName = getBridgeClassName();
136    
137        if (bridgeClassName != null)
138        {
139          try
140          {
141            ClassLoader loader = Thread.currentThread().getContextClassLoader();
142            mFacesBridgeClass = (Class<? extends Bridge>) loader.loadClass(bridgeClassName);
143          } catch (ClassNotFoundException cnfe)
144          {
145            // Do nothing and fall through to null check
146          }
147        }
148    
149        if (mFacesBridgeClass == null)
150        {
151          throw new PortletException("Configuration Error: Initial Parameter '" + BRIDGE_CLASS + 
152                                     "' is not defined for portlet: " + getPortletName());
153        }
154    
155        // Get the other bridge configuration parameters and set as context attributes
156        List<String> excludedAttrs = getExcludedRequestAttributes();
157        if (excludedAttrs != null)
158        {
159          getPortletContext().setAttribute(Bridge.BRIDGE_PACKAGE_PREFIX + getPortletName() + "." + 
160                                           Bridge.EXCLUDED_REQUEST_ATTRIBUTES, excludedAttrs);
161        }
162    
163        Boolean preserveActionParams = new Boolean(isPreserveActionParameters());
164        getPortletContext().setAttribute(Bridge.BRIDGE_PACKAGE_PREFIX + getPortletName() + "." + 
165                                         Bridge.PRESERVE_ACTION_PARAMS, preserveActionParams);
166    
167        Map defaultViewIdMap = getDefaultViewIdMap();
168        getPortletContext().setAttribute(Bridge.BRIDGE_PACKAGE_PREFIX + getPortletName() + "." + 
169                                         Bridge.DEFAULT_VIEWID_MAP, defaultViewIdMap);
170    
171        // Don't instanciate/initialize the bridge yet. Do it on first use
172      }
173    
174      /**
175       * Release resources, specifically it destroys the bridge.
176       */
177      @Override
178      public void destroy()
179      {
180        if (mFacesBridge != null)
181        {
182          mFacesBridge.destroy();
183          mFacesBridge = null;
184          mFacesBridgeClass = null;
185        }
186        mDefaultViewIdMap = null;
187      }
188    
189      /**
190       * If mode is VIEW, EDIT, or HELP -- defer to the doView, doEdit, doHelp so subclasses can
191       * override. Otherwise handle mode here if there is a defaultViewId mapping for it.
192       */
193      @Override
194      public void doDispatch(RenderRequest request, RenderResponse response) throws PortletException, 
195                                                                                    IOException
196      {
197        // Defer to helper methods for standard modes so subclasses can override
198        PortletMode mode = request.getPortletMode();
199        if (mode == PortletMode.EDIT || mode == PortletMode.HELP || mode == PortletMode.VIEW)
200        {
201          super.doDispatch(request, response);
202        } else
203        {
204          // Bridge didn't process this one -- so forge ahead
205          if (!doRenderDispatchInternal(request, response))
206          {
207            super.doDispatch(request, response);
208          }
209        }
210      }
211    
212      @Override
213      protected void doEdit(RenderRequest request, RenderResponse response) throws PortletException, 
214                                                                                   java.io.IOException
215      {
216        doRenderDispatchInternal(request, response);
217      }
218    
219      @Override
220      protected void doHelp(RenderRequest request, RenderResponse response) throws PortletException, 
221                                                                                   java.io.IOException
222      {
223        doRenderDispatchInternal(request, response);
224      }
225    
226      @Override
227      protected void doView(RenderRequest request, RenderResponse response) throws PortletException, 
228                                                                                   java.io.IOException
229      {
230        doRenderDispatchInternal(request, response);
231      }
232    
233      @Override
234      public void processAction(ActionRequest request, 
235                                ActionResponse response) throws PortletException, IOException
236      {
237        doActionDispatchInternal(request, response);
238      }
239    
240      /**
241       * Returns the set of RequestAttribute names that the portlet wants the bridge to
242       * exclude from its managed request scope.  This default implementation picks up
243       * this list from the comma delimited init_param javax.portlet.faces.excludedRequestAttributes.
244       * 
245       * @return a List containing the names of the attributes to be excluded. null if it can't be
246       *         determined.
247       */
248      public List<String> getExcludedRequestAttributes()
249      {
250        String excludedAttrs = 
251          getPortletConfig().getInitParameter(Bridge.BRIDGE_PACKAGE_PREFIX + Bridge.EXCLUDED_REQUEST_ATTRIBUTES);
252        if (excludedAttrs == null)
253        {
254          return null;
255        }
256    
257        String[] attrArray = excludedAttrs.split(",");
258        // process comma delimited String into a List
259        ArrayList<String> list = new ArrayList(attrArray.length);
260        for (int i = 0; i < attrArray.length; i++)
261        {
262          list.add(attrArray[i]);
263        }
264        return list;
265      }
266    
267      /**
268       * Returns a boolean indicating whether or not the bridge should preserve all the
269       * action parameters in the subsequent renders that occur in the same scope.  This
270       * default implementation reads the values from the portlet init_param
271       * javax.portlet.faces.preserveActionParams.  If not present, false is returned.
272       * 
273       * @return a boolean indicating whether or not the bridge should preserve all the
274       * action parameters in the subsequent renders that occur in the same scope.
275       */
276      public boolean isPreserveActionParameters()
277      {
278        String preserveActionParams = 
279          getPortletConfig().getInitParameter(Bridge.BRIDGE_PACKAGE_PREFIX + 
280                                              Bridge.PRESERVE_ACTION_PARAMS);
281        if (preserveActionParams == null)
282        {
283          return false;
284        } else
285        {
286          return Boolean.parseBoolean(preserveActionParams);
287        }
288      }
289    
290      /**
291       * Returns the className of the bridge implementation this portlet uses. Subclasses override to
292       * alter the default behavior. Default implementation first checks for a portlet context init
293       * parameter: javax.portlet.faces.BridgeImplClass. If it doesn't exist then it looks for the
294       * resource file "META-INF/services/javax.portlet.faces.Bridge" using the current threads
295       * classloader and extracts the classname from the first line in that file.
296       * 
297       * @return the class name of the Bridge class the GenericFacesPortlet uses. null if it can't be
298       *         determined.
299       */
300      public String getBridgeClassName()
301      {
302        String bridgeClassName = getPortletConfig().getPortletContext().getInitParameter(BRIDGE_CLASS);
303    
304        if (bridgeClassName == null)
305        {
306          bridgeClassName = 
307              getFromServicesPath(getPortletConfig().getPortletContext(), BRIDGE_SERVICE_CLASSPATH);
308        }
309        return bridgeClassName;
310      }
311    
312      /**
313       * Returns the default content type for this portlet request. Subclasses override to
314       * alter the default behavior. Default implementation returns value of the portlet context init
315       * parameter: javax.portlet.faces.DefaultContentType. If it doesn't exist the portlet
316       * request's preferred response content type is returned.
317       * 
318       * Note:  This support is specific to the Portlet 1.0 Bridge.  Its value is
319       * likely to be ignored by the Portlet 2.0 Bridge or later.
320       * 
321       * @return the content type that should be used for this response.
322       */
323      public String getResponseContentType(PortletRequest request)
324      {
325        String contentType = 
326          getPortletConfig().getPortletContext().getInitParameter(DEFAULT_CONTENT_TYPE);
327    
328        if (contentType == null)
329        {
330          contentType = request.getResponseContentType();
331        }
332        return contentType;
333      }
334    
335      /**
336       * Returns the character set encoding used for this portlet response. Subclasses override to
337       * alter the default behavior. Default implementation returns value of the portlet context init
338       * parameter: javax.portlet.faces.DefaultCharacterSetEncoding. If it doesn't exist null
339       * is returned.
340       * 
341       * Note:  This support is specific to the Portlet 1.0 Bridge.  Its value is
342       * likely to be ignored by the Portlet 2.0 Bridge or later.
343       * 
344       * @return the content type that should be used for this response.
345       */
346      public String getResponseCharacterSetEncoding(PortletRequest request)
347      {
348        return getPortletConfig().getPortletContext().getInitParameter(DEFAULT_CHARACTERSET_ENCODING);
349      }
350    
351    
352      /**
353       * Returns the defaultViewIdMap the bridge should use when its unable to resolve to a specific
354       * target in the incoming request. There is one entry per support <code>PortletMode
355       * </code>.  The entry key is the name of the mode.  The entry value is the default viewId
356       * for that mode.
357       * 
358       * @return the defaultViewIdMap
359       */
360      public Map getDefaultViewIdMap()
361      {
362        if (mDefaultViewIdMap == null)
363        {
364          mDefaultViewIdMap = new HashMap<String, String>();
365          // loop through all portlet initialization parameters looking for those in the
366          // correct form
367          PortletConfig config = getPortletConfig();
368    
369          Enumeration<String> e = config.getInitParameterNames();
370          int len = DEFAULT_VIEWID.length();
371          while (e.hasMoreElements())
372          {
373            String s = e.nextElement();
374            if (s.startsWith(DEFAULT_VIEWID) && s.length() > DEFAULT_VIEWID.length())
375            {
376              String viewId = config.getInitParameter(s);
377              // extract the mode
378              s = s.substring(len + 1);
379              mDefaultViewIdMap.put(s, viewId);
380            }
381          }
382        }
383    
384        return mDefaultViewIdMap;
385      }
386    
387      private boolean isNonFacesRequest(PortletRequest request, PortletResponse response)
388      {
389        // Non Faces request is identified by either the presence of the _jsfBridgeNonFacesView
390        // parameter or the request being for a portlet mode which doesn't have a default
391        // Faces view configured for it.
392        if (request.getParameter(Bridge.NONFACES_TARGET_PATH_PARAMETER) != null)
393        {
394          return true;
395        }
396    
397        String modeDefaultViewId = mDefaultViewIdMap.get(request.getPortletMode().toString());
398        return modeDefaultViewId == null;
399      }
400    
401      private void doActionDispatchInternal(ActionRequest request, 
402                                            ActionResponse response) throws PortletException, 
403                                                                            IOException
404      {
405        // First determine whether this is a Faces or nonFaces request
406        if (isNonFacesRequest(request, response))
407        {
408          throw new PortletException("GenericFacesPortlet:  Action request is not for a Faces target.  Such nonFaces requests must be handled by a subclass.");
409        } else
410        {
411          doBridgeDispatch(request, response);
412        }
413      }
414    
415      private boolean doRenderDispatchInternal(RenderRequest request, 
416                                               RenderResponse response) throws PortletException, 
417                                                                               IOException
418      {
419        // First determine whether this is a Faces or nonFaces request
420        if (isNonFacesRequest(request, response))
421        {
422          return doNonFacesDispatch(request, response);
423        } else
424        {
425          WindowState state = request.getWindowState();
426          if (!state.equals(WindowState.MINIMIZED))
427          {
428            doBridgeDispatch(request, response);
429          }
430          return true;
431        }
432      }
433    
434      private boolean doNonFacesDispatch(RenderRequest request, 
435                                         RenderResponse response) throws PortletException
436      {
437        // Can only dispatch if the path is encoded in the request parameter
438        String targetPath = request.getParameter(Bridge.NONFACES_TARGET_PATH_PARAMETER);
439        if (targetPath == null)
440        {
441          // Didn't handle this request
442          return false;
443        }
444    
445        // merely dispatch this to the nonJSF target
446        // but because this is portlet 1.0 we have to ensure the content type is set.
447        // Ensure the ContentType is set before rendering
448        if (response.getContentType() == null)
449        {
450          response.setContentType(request.getResponseContentType());
451        }
452        try
453        {
454          PortletRequestDispatcher dispatcher = 
455            this.getPortletContext().getRequestDispatcher(targetPath);
456          dispatcher.include(request, response);
457          return true;
458        } catch (Exception e)
459        {
460          throw new PortletException("Unable to dispatch to: " + targetPath, e);
461        }
462      }
463    
464      private void doBridgeDispatch(RenderRequest request, 
465                                    RenderResponse response) throws PortletException
466      {
467        // initial Bridge if not already active
468        initBridgeRequest(request, response);
469    
470        // Set the response ContentType/CharacterSet
471        setResponseContentType(response, getResponseContentType(request), 
472                               getResponseCharacterSetEncoding(request));
473    
474        try
475        {
476          mFacesBridge.doFacesRequest(request, response);
477        } catch (BridgeException e)
478        {
479          throw new PortletException("doBridgeDispatch failed:  error from Bridge in executing the request", 
480                                     e);
481        }
482    
483      }
484    
485      private void doBridgeDispatch(ActionRequest request, 
486                                    ActionResponse response) throws PortletException
487      {
488        // initial Bridge if not already active
489        initBridgeRequest(request, response);
490    
491        try
492        {
493          mFacesBridge.doFacesRequest(request, response);
494        } catch (BridgeException e)
495        {
496          throw new PortletException("doBridgeDispatch failed:  error from Bridge in executing the request", 
497                                     e);
498        }
499    
500      }
501    
502      private void initBridgeRequest(PortletRequest request, 
503                                     PortletResponse response) throws PortletException
504      {
505        initBridge();
506    
507    
508        // Now do any per request initialization
509        // I nthis case look to see if the request is encoded (usually 
510        // from a NonFaces view response) with the specific Faces
511        // view to execute.
512        String view = request.getParameter(Bridge.FACES_VIEW_ID_PARAMETER);
513        if (view != null)
514        {
515          request.setAttribute(Bridge.VIEW_ID, view);
516        } else
517        {
518          view = request.getParameter(Bridge.FACES_VIEW_PATH_PARAMETER);
519          if (view != null)
520          {
521            request.setAttribute(Bridge.VIEW_PATH, view);
522          }
523        }
524      }
525    
526      private void initBridge() throws PortletException
527      {
528        // Ensure te Bridge has been constrcuted and initialized
529        if (mFacesBridge == null)
530        {
531          try
532          {
533            mFacesBridge = mFacesBridgeClass.newInstance();
534            mFacesBridge.init(getPortletConfig());
535          } catch (Exception e)
536          {
537            throw new PortletException("doBridgeDisptach:  error instantiating the bridge class", e);
538          }
539        }
540      }
541    
542      private void setResponseContentType(RenderResponse response, String contentType, 
543                                          String charSetEncoding)
544      {
545        if (contentType == null)
546        {
547          return;
548    
549        }
550        if (charSetEncoding != null)
551        {
552          StringBuffer buf = new StringBuffer(contentType);
553          buf.append(";");
554          buf.append(charSetEncoding);
555          response.setContentType(buf.toString());
556        } else
557        {
558          response.setContentType(contentType);
559        }
560      }
561    
562      private String getFromServicesPath(PortletContext context, String resourceName)
563      {
564        // Check for a services definition
565        String result = null;
566        BufferedReader reader = null;
567        InputStream stream = null;
568        try
569        {
570          ClassLoader cl = Thread.currentThread().getContextClassLoader();
571          if (cl == null)
572          {
573            return null;
574          }
575    
576          stream = cl.getResourceAsStream(resourceName);
577          if (stream != null)
578          {
579            // Deal with systems whose native encoding is possibly
580            // different from the way that the services entry was created
581            try
582            {
583              reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
584            } catch (UnsupportedEncodingException e)
585            {
586              reader = new BufferedReader(new InputStreamReader(stream));
587            }
588            result = reader.readLine();
589            if (result != null)
590            {
591              result = result.trim();
592            }
593            reader.close();
594            reader = null;
595            stream = null;
596          }
597        } catch (IOException e)
598        {
599        } catch (SecurityException e)
600        {
601        } finally
602        {
603          if (reader != null)
604          {
605            try
606            {
607              reader.close();
608              stream = null;
609            } catch (Throwable t)
610            {
611              ;
612            }
613            reader = null;
614          }
615          if (stream != null)
616          {
617            try
618            {
619              stream.close();
620            } catch (Throwable t)
621            {
622              ;
623            }
624            stream = null;
625          }
626        }
627        return result;
628      }
629    
630    }