View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.application;
23  
24  import java.util.Locale;
25  import java.util.UUID;
26  import java.util.logging.Level;
27  import java.util.logging.Logger;
28  
29  import javax.el.ELContext;
30  import javax.el.ExpressionFactory;
31  import javax.el.ValueExpression;
32  import javax.faces.FacesException;
33  import javax.faces.application.ProjectStage;
34  import javax.faces.application.Resource;
35  import javax.faces.component.UIOutput;
36  import javax.faces.context.ExternalContext;
37  import javax.faces.context.FacesContext;
38  import javax.faces.event.PhaseEvent;
39  import javax.faces.event.PhaseId;
40  import javax.faces.event.PhaseListener;
41  
42  import org.primefaces.config.PrimeConfiguration;
43  import org.primefaces.context.PrimeApplicationContext;
44  import org.primefaces.context.PrimeRequestContext;
45  import org.primefaces.util.LocaleUtils;
46  
47  /**
48   * Creates a custom PhaseListener for RENDER_RESPONSE phase which will during beforePhase() dynamically add those PrimeFaces resources via
49   * UIViewRoot#addComponentResource(). This will run far before those @ResourceDependency annotations are processed. This satisfies PrimeFaces' intent of having
50   * those hardcoded resources to be rendered before of the dependencies of their components.
51   * <p>
52   * Register it as below in faces-config.xml:
53   * </p>
54   * 
55   * <pre>
56   *   &lt;lifecycle&gt;
57   *      &lt;phase-listener&gt;org.primefaces.extensions.application.PrimeFacesResourceProcessor&lt;/phase-listener&gt;
58   *   &lt;/lifecycle&gt;
59   * </pre>
60   *
61   * @see <a href="https://github.com/omnifaces/omnifaces/wiki/Combine-hardcoded-PrimeFaces-resources-using-CombinedResourceHandler">OmniFaces</a>
62   * @since 10.0.0
63   */
64  public class PrimeFacesResourceProcessor implements PhaseListener {
65  
66      private static final long serialVersionUID = 1L;
67      private static final Logger LOGGER = Logger.getLogger(PrimeFacesResourceProcessor.class.getName());
68      private static final String LIBRARY = "primefaces";
69  
70      @Override
71      public PhaseId getPhaseId() {
72          return PhaseId.RENDER_RESPONSE;
73      }
74  
75      @Override
76      public void afterPhase(final PhaseEvent event) {
77          // NOOP.
78      }
79  
80      @Override
81      public void beforePhase(final PhaseEvent event) {
82          final FacesContext context = event.getFacesContext();
83          final PrimeRequestContext requestContext = PrimeRequestContext.getCurrentInstance(context);
84          final PrimeApplicationContext applicationContext = requestContext.getApplicationContext();
85          final PrimeConfiguration configuration = applicationContext.getConfig();
86  
87          final String theme;
88          final String themeParamValue = configuration.getTheme();
89  
90          if (themeParamValue != null) {
91              final ELContext elContext = context.getELContext();
92              final ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory();
93              final ValueExpression ve = expressionFactory.createValueExpression(elContext, themeParamValue,
94                          String.class);
95              theme = (String) ve.getValue(elContext);
96          }
97          else {
98              theme = "saga"; // default
99          }
100 
101         if (theme != null && !"none".equals(theme)) {
102             encodeCSS(context, LIBRARY + "-" + theme, "theme.css");
103         }
104 
105         // Icons
106         if (configuration.isPrimeIconsEnabled()) {
107             encodeCSS(context, LIBRARY, "primeicons/primeicons.css");
108         }
109 
110         // normal CSV is a required dependency for some special components like fileupload
111         encodeValidationResources(context, configuration);
112 
113         if (configuration.isClientSideLocalizationEnabled()) {
114             try {
115                 final Locale locale = LocaleUtils.getCurrentLocale(context);
116                 encodeJS(context, "locales/locale-" + locale.getLanguage() + ".js");
117             }
118             catch (FacesException e) {
119                 if (context.isProjectStage(ProjectStage.Development)) {
120                     LOGGER.log(Level.WARNING,
121                                 "Failed to load client side locale.js. {0}", e.getMessage());
122                 }
123             }
124         }
125     }
126 
127     protected void encodeValidationResources(final FacesContext context, final PrimeConfiguration configuration) {
128         // normal CSV is a required dependency for some special components like fileupload
129         encodeJS(context, "validation/validation.js");
130 
131         if (configuration.isClientSideValidationEnabled()) {
132             // moment is needed for Date validation
133             encodeJS(context, "moment/moment.js");
134 
135             // BV CSV is optional and must be enabled by config
136             if (configuration.isBeanValidationEnabled()) {
137                 encodeJS(context, "validation/validation.bv.js");
138             }
139         }
140     }
141 
142     private void encodeCSS(final FacesContext context, final String library, final String name) {
143         final Resource resource = context.getApplication().getResourceHandler().createResource(name, library);
144         if (resource == null) {
145             throw new FacesException(
146                         "Error loading CSS, cannot find \"" + name + "\" resource of \"" + library + "\" library");
147         }
148         final UIOutput css = new UIOutput();
149         css.setId("css-" + UUID.randomUUID());
150         css.setRendererType("javax.faces.resource.Stylesheet");
151         css.getAttributes().put("library", library);
152         css.getAttributes().put("name", name);
153         final String target = getTarget(context.getExternalContext(), "css");
154         context.getViewRoot().addComponentResource(context, css, target);
155     }
156 
157     private void encodeJS(final FacesContext context, final String name) {
158         final Resource resource = context.getApplication().getResourceHandler().createResource(name, LIBRARY);
159         if (resource == null) {
160             throw new FacesException("Error loading JavaScript, cannot find \"" + name + "\" resource of \"" + LIBRARY +
161                         "\" library");
162         }
163         final UIOutput js = new UIOutput();
164         js.setId("js-" + UUID.randomUUID());
165         js.setRendererType("javax.faces.resource.Script");
166         js.getAttributes().put("library", LIBRARY);
167         js.getAttributes().put("name", name);
168         final String target = getTarget(context.getExternalContext(), "js");
169         context.getViewRoot().addComponentResource(context, js, target);
170     }
171 
172     /**
173      * Return "head" to capture in Omnifaces CombinedResourceHandler or "body" to not capture it.
174      *
175      * @param externalContext the Faces external context
176      * @param type the type either "css" or "js"
177      * @return either "head" or "body"
178      */
179     private String getTarget(final ExternalContext externalContext, final String type) {
180         final String parameter = String.format("primefaces.%s.COMBINED_RESOURCE_HANDLER_DISABLED", type);
181         final String value = externalContext.getInitParameter(parameter);
182         final boolean disabled = Boolean.parseBoolean(value);
183         return disabled ? "body" : "head";
184     }
185 
186 }