001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.apache.geronimo.myfaces.deployment;
019    
020    import java.io.IOException;
021    import java.net.MalformedURLException;
022    import java.net.URL;
023    import java.util.ArrayList;
024    import java.util.Collection;
025    import java.util.HashMap;
026    import java.util.List;
027    import java.util.Map;
028    import java.util.StringTokenizer;
029    import java.util.jar.JarFile;
030    
031    import javax.faces.webapp.FacesServlet;
032    
033    import org.apache.commons.logging.Log;
034    import org.apache.commons.logging.LogFactory;
035    import org.apache.geronimo.common.DeploymentException;
036    import org.apache.geronimo.deployment.ModuleIDBuilder;
037    import org.apache.geronimo.deployment.service.EnvironmentBuilder;
038    import org.apache.geronimo.deployment.util.DeploymentUtil;
039    import org.apache.geronimo.deployment.xmlbeans.XmlBeansUtil;
040    import org.apache.geronimo.gbean.AbstractName;
041    import org.apache.geronimo.gbean.AbstractNameQuery;
042    import org.apache.geronimo.gbean.GBeanData;
043    import org.apache.geronimo.gbean.GBeanInfo;
044    import org.apache.geronimo.gbean.GBeanInfoBuilder;
045    import org.apache.geronimo.j2ee.annotation.Holder;
046    import org.apache.geronimo.j2ee.deployment.EARContext;
047    import org.apache.geronimo.j2ee.deployment.Module;
048    import org.apache.geronimo.j2ee.deployment.ModuleBuilderExtension;
049    import org.apache.geronimo.j2ee.deployment.NamingBuilder;
050    import org.apache.geronimo.j2ee.deployment.WebModule;
051    import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory;
052    import org.apache.geronimo.kernel.GBeanAlreadyExistsException;
053    import org.apache.geronimo.kernel.Naming;
054    import org.apache.geronimo.kernel.config.Configuration;
055    import org.apache.geronimo.kernel.config.ConfigurationStore;
056    import org.apache.geronimo.kernel.repository.Environment;
057    import org.apache.geronimo.myfaces.LifecycleProviderGBean;
058    import org.apache.geronimo.schema.SchemaConversionUtils;
059    import org.apache.geronimo.xbeans.javaee.FacesConfigDocument;
060    import org.apache.geronimo.xbeans.javaee.FacesConfigManagedBeanType;
061    import org.apache.geronimo.xbeans.javaee.FacesConfigType;
062    import org.apache.geronimo.xbeans.javaee.FullyQualifiedClassType;
063    import org.apache.geronimo.xbeans.javaee.ParamValueType;
064    import org.apache.geronimo.xbeans.javaee.ServletType;
065    import org.apache.geronimo.xbeans.javaee.WebAppType;
066    import org.apache.geronimo.xbeans.javaee.ListenerType;
067    import org.apache.myfaces.webapp.StartupServletContextListener;
068    import org.apache.xbean.finder.ClassFinder;
069    import org.apache.xmlbeans.XmlCursor;
070    import org.apache.xmlbeans.XmlException;
071    import org.apache.xmlbeans.XmlObject;
072    
073    /**
074     * @version $Rev $Date
075     */
076    public class MyFacesModuleBuilderExtension implements ModuleBuilderExtension {
077    
078        private static final Log log = LogFactory.getLog(MyFacesModuleBuilderExtension.class);
079    
080        private final Environment defaultEnvironment;
081        private final AbstractNameQuery providerFactoryNameQuery;
082        private final NamingBuilder namingBuilders;
083        private static final String CONTEXT_LISTENER_NAME = StartupServletContextListener.class.getName();
084        private static final String FACES_SERVLET_NAME = FacesServlet.class.getName();
085        private static final String SCHEMA_LOCATION_URL = "http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd";
086        private static final String VERSION = "1.2";
087    
088    
089        public MyFacesModuleBuilderExtension(Environment defaultEnvironment, AbstractNameQuery providerFactoryNameQuery, NamingBuilder namingBuilders) {
090            this.defaultEnvironment = defaultEnvironment;
091            this.providerFactoryNameQuery = providerFactoryNameQuery;
092            this.namingBuilders = namingBuilders;
093        }
094    
095        public void createModule(Module module, Object plan, JarFile moduleFile, String targetPath, URL specDDUrl, Environment environment, Object moduleContextInfo, AbstractName earName, Naming naming, ModuleIDBuilder idBuilder) throws DeploymentException {
096            if (!(module instanceof WebModule)) {
097                //not a web module, nothing to do
098                return;
099            }
100            WebModule webModule = (WebModule) module;
101            WebAppType webApp = (WebAppType) webModule.getSpecDD();
102            if (!hasFacesServlet(webApp)) {
103                return;
104            }
105    
106            EnvironmentBuilder.mergeEnvironments(environment, defaultEnvironment);
107        }
108    
109        public void installModule(JarFile earFile, EARContext earContext, Module module, Collection configurationStores, ConfigurationStore targetConfigurationStore, Collection repository) throws DeploymentException {
110        }
111    
112        public void initContext(EARContext earContext, Module module, ClassLoader cl) throws DeploymentException {
113        }
114    
115        public void addGBeans(EARContext earContext, Module module, ClassLoader cl, Collection repository) throws DeploymentException {
116            if (!(module instanceof WebModule)) {
117                //not a web module, nothing to do
118                return;
119            }
120            WebModule webModule = (WebModule) module;
121            WebAppType webApp = (WebAppType) webModule.getSpecDD();
122            if (!hasFacesServlet(webApp)) {
123                return;
124            }
125    
126            EARContext moduleContext = module.getEarContext();
127            Map sharedContext = module.getSharedContext();
128            //add the ServletContextListener to the web app context
129            GBeanData webAppData = (GBeanData) sharedContext.get(WebModule.WEB_APP_DATA);
130            //jetty specific support
131            Object value = webAppData.getAttribute("listenerClassNames");
132            if (value instanceof Collection && !((Collection) value).contains(CONTEXT_LISTENER_NAME)) {
133                ((Collection<String>) value).add(CONTEXT_LISTENER_NAME);
134            } else {
135                //try to add listener to the web app xml
136                ListenerType listenerType = webApp.addNewListener();
137                FullyQualifiedClassType className = listenerType.addNewListenerClass();
138                className.setStringValue(CONTEXT_LISTENER_NAME);
139            }
140            AbstractName moduleName = moduleContext.getModuleName();
141            Map<NamingBuilder.Key, Object> buildingContext = new HashMap<NamingBuilder.Key, Object>();
142            buildingContext.put(NamingBuilder.GBEAN_NAME_KEY, moduleName);
143    
144            //use the same jndi context as the web app
145            Map compContext = NamingBuilder.JNDI_KEY.get(module.getSharedContext());
146            buildingContext.put(NamingBuilder.JNDI_KEY, compContext);
147    
148            //use the same holder object as the web app.
149            Holder holder = NamingBuilder.INJECTION_KEY.get(sharedContext);
150            buildingContext.put(NamingBuilder.INJECTION_KEY, holder);
151    
152            XmlObject jettyWebApp = webModule.getVendorDD();
153    
154            Configuration earConfiguration = earContext.getConfiguration();
155    
156            ClassFinder classFinder = createMyFacesClassFinder(webApp, webModule);
157            webModule.setClassFinder(classFinder);
158    
159            namingBuilders.buildNaming(webApp, jettyWebApp, webModule, buildingContext);
160    
161            AbstractName providerName = moduleContext.getNaming().createChildName(moduleName, "jsf-lifecycle", "jsf");
162            GBeanData providerData = new GBeanData(providerName, LifecycleProviderGBean.GBEAN_INFO);
163            providerData.setAttribute("holder", holder);
164            providerData.setAttribute("componentContext", compContext);
165            providerData.setReferencePattern("LifecycleProviderFactory", providerFactoryNameQuery);
166            try {
167                moduleContext.addGBean(providerData);
168            } catch (GBeanAlreadyExistsException e) {
169                throw new DeploymentException("Duplicate jsf config gbean in web module", e);
170            }
171    
172            //make the web app start second after the injection machinery
173            webAppData.addDependency(providerName);
174    
175        }
176    
177        private boolean hasFacesServlet(WebAppType webApp) {
178            for (ServletType servlet : webApp.getServletArray()) {
179                if (servlet.isSetServletClass() && FACES_SERVLET_NAME.equals(servlet.getServletClass().getStringValue().trim())) {
180                    return true;
181                }
182            }
183            return false;
184        }
185    
186    
187        protected ClassFinder createMyFacesClassFinder(WebAppType webApp, WebModule webModule) throws DeploymentException {
188    
189            List<Class> classes = getFacesClasses(webApp, webModule);
190            return new ClassFinder(classes);
191        }
192    
193    
194        /**
195         * getFacesConfigFileURL()
196         * <p/>
197         * <p>Locations to search for the MyFaces configuration file(s):
198         * <ol>
199         * <li>META-INF/faces-config.xml
200         * <li>WEB-INF/faces-config.xml
201         * <li>javax.faces.CONFIG_FILES -- Context initialization param of Comma separated
202         * list of URIs of (additional) faces config files
203         * </ol>
204         * <p/>
205         * <p><strong>Notes:</strong>
206         * <ul>
207         * </ul>
208         *
209         * @param webApp    spec DD for module
210         * @param webModule module being deployed
211         * @return list of all managed bean classes from all faces-config xml files.
212         * @throws org.apache.geronimo.common.DeploymentException
213         *          if a faces-config.xml file is located but cannot be parsed.
214         */
215        private List<Class> getFacesClasses(WebAppType webApp, WebModule webModule) throws DeploymentException {
216            log.debug("getFacesClasses( " + webApp.toString() + "," + '\n' +
217                               (webModule != null ? webModule.getName() : null) + " ): Entry");
218    
219            // Get the classloader from the module's EARContext
220            ClassLoader classLoader = webModule.getEarContext().getClassLoader();
221    
222            // 1. META-INF/faces-config.xml
223            List<Class> classes = new ArrayList<Class>();
224            try {
225                URL url = DeploymentUtil.createJarURL(webModule.getModuleFile(), "META-INF/faces-config.xml");
226                parseConfigFile(url, classLoader, classes);
227            } catch (MalformedURLException mfe) {
228                throw new DeploymentException("Could not locate META-INF/faces-config.xml" + mfe.getMessage(), mfe);
229            }
230    
231            // 2. WEB-INF/faces-config.xml
232            try {
233                URL url = DeploymentUtil.createJarURL(webModule.getModuleFile(), "WEB-INF/faces-config.xml");
234                parseConfigFile(url, classLoader, classes);
235            } catch (MalformedURLException mfe) {
236                throw new DeploymentException("Could not locate WEB-INF/faces-config.xml" + mfe.getMessage(), mfe);
237            }
238    
239            // 3. javax.faces.CONFIG_FILES
240            ParamValueType[] paramValues = webApp.getContextParamArray();
241            for (ParamValueType paramValue : paramValues) {
242                if (paramValue.getParamName().getStringValue().trim().equals("javax.faces.CONFIG_FILES")) {
243                    String configFiles = paramValue.getParamValue().getStringValue().trim();
244                    StringTokenizer st = new StringTokenizer(configFiles, ",", false);
245                    while (st.hasMoreTokens()) {
246                        String configfile = st.nextToken().trim();
247                        if (!configfile.equals("")) {
248                            if (configfile.startsWith("/")) {
249                                configfile = configfile.substring(1);
250                            }
251                            try {
252                                URL url = DeploymentUtil.createJarURL(webModule.getModuleFile(), configfile);
253                                parseConfigFile(url, classLoader, classes);
254                            } catch (MalformedURLException mfe) {
255                                throw new DeploymentException("Could not locate config file " + configfile + ", " + mfe.getMessage(), mfe);
256                            }
257                        }
258                    }
259                    break;
260                }
261            }
262    
263            log.debug("getFacesClasses() Exit: " + classes.size() + " " + classes.toString());
264            return classes;
265        }
266    
267        private void parseConfigFile(URL url, ClassLoader classLoader, List<Class> classes) throws DeploymentException {
268            log.debug("parseConfigFile( " + url.toString() + " ): Entry");
269    
270            try {
271                XmlObject xml = XmlBeansUtil.parse(url, null);
272                FacesConfigDocument fcd = convertToFacesConfigSchema(xml);
273                FacesConfigType facesConfig = fcd.getFacesConfig();
274    
275                // Get all the managed beans from the faces configuration file
276                FacesConfigManagedBeanType[] managedBeans = facesConfig.getManagedBeanArray();
277                for (FacesConfigManagedBeanType managedBean : managedBeans) {
278                    FullyQualifiedClassType cls = managedBean.getManagedBeanClass();
279                    String className = cls.getStringValue().trim();
280                    Class<?> clas;
281                    try {
282                        clas = classLoader.loadClass(className);
283                        classes.add(clas);
284                    }
285                    catch (ClassNotFoundException e) {
286                        log.warn("MyFacesModuleBuilderExtension: Could not load managed bean class: " + className  + " mentioned in faces-config.xml file at " + url.toString());
287                    }
288                }
289            }
290            catch (XmlException xmle) {
291                throw new DeploymentException("Could not parse alleged faces-config.xml at " + url.toString(), xmle);
292            }
293            catch (IOException ioe) {
294                //config file does not exist
295            }
296    
297            log.debug("parseConfigFile(): Exit");
298        }
299    
300        protected static FacesConfigDocument convertToFacesConfigSchema(XmlObject xmlObject) throws XmlException {
301            log.debug("convertToFacesConfigSchema( " + xmlObject.toString() + " ): Entry");
302            XmlCursor cursor = xmlObject.newCursor();
303            try {
304                cursor.toStartDoc();
305                cursor.toFirstChild();
306                if (SchemaConversionUtils.JAVAEE_NAMESPACE.equals(cursor.getName().getNamespaceURI())) {
307                    //do nothing
308                } else if (SchemaConversionUtils.J2EE_NAMESPACE.equals(cursor.getName().getNamespaceURI())) {
309                    SchemaConversionUtils.convertSchemaVersion(cursor, SchemaConversionUtils.JAVAEE_NAMESPACE, SCHEMA_LOCATION_URL, VERSION);
310                } else {
311                // otherwise assume DTD
312                    SchemaConversionUtils.convertToSchema(cursor, SchemaConversionUtils.JAVAEE_NAMESPACE, SCHEMA_LOCATION_URL, VERSION);
313                }
314            }
315            finally {
316                cursor.dispose();
317            }
318            XmlObject result = xmlObject.changeType(FacesConfigDocument.type);
319            if (result != null) {
320                XmlBeansUtil.validateDD(result);
321                log.debug("convertToFacesConfigSchema(): Exit 2" );
322                return(FacesConfigDocument) result;
323            }
324            XmlBeansUtil.validateDD(xmlObject);
325            log.debug("convertToFacesConfigSchema(): Exit 3" );
326            return(FacesConfigDocument) xmlObject;
327        }
328    
329        public static final GBeanInfo GBEAN_INFO;
330    
331        static {
332            GBeanInfoBuilder infoBuilder = GBeanInfoBuilder.createStatic(MyFacesModuleBuilderExtension.class, NameFactory.MODULE_BUILDER);
333            infoBuilder.addAttribute("defaultEnvironment", Environment.class, true, true);
334            infoBuilder.addAttribute("providerFactoryNameQuery", AbstractNameQuery.class, true, true);
335            infoBuilder.addReference("NamingBuilders", NamingBuilder.class, NameFactory.MODULE_BUILDER);
336    
337            infoBuilder.setConstructor(new String[]{
338                    "defaultEnvironment",
339                    "providerFactoryNameQuery",
340                    "NamingBuilders"});
341            GBEAN_INFO = infoBuilder.getBeanInfo();
342        }
343    
344        public static GBeanInfo getGBeanInfo() {
345            return GBEAN_INFO;
346        }
347    
348    
349    }