Author: lu4242
Date: Thu Dec 17 00:42:18 2009
New Revision: 891494
URL: http://svn.apache.org/viewvc?rev=891494&view=rev
Log:
MYFACES-2460 Add Resource Headers and allow EL Expressions only on css files
Added:
myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceUtils.java
Modified:
myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/DefaultResourceHandlerSupport.java
myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/ResourceHandlerSupport.java
myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceImpl.java
Modified: myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/DefaultResourceHandlerSupport.java
URL: http://svn.apache.org/viewvc/myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/DefaultResourceHandlerSupport.java?rev=891494&r1=891493&r2=891494&view=diff
==============================================================================
--- myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/DefaultResourceHandlerSupport.java
(original)
+++ myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/DefaultResourceHandlerSupport.java
Thu Dec 17 00:42:18 2009
@@ -24,6 +24,7 @@
import javax.faces.context.FacesContext;
import org.apache.myfaces.application.DefaultViewHandlerSupport.FacesServletMapping;
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
import org.apache.myfaces.resource.ClassLoaderResourceLoader;
import org.apache.myfaces.resource.ExternalContextResourceLoader;
import org.apache.myfaces.resource.ResourceLoader;
@@ -37,7 +38,14 @@
*/
public class DefaultResourceHandlerSupport implements ResourceHandlerSupport
{
-
+
+ /**
+ * Set the max time in miliseconds set on the "Expires" header for a resource.
+ * (default to one week in miliseconds or 604800000)
+ */
+ @JSFWebConfigParam(since="2.0", defaultValue="604800000")
+ public static final String RESOURCE_MAX_TIME_EXPIRES = "org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES";
+
/**
* Identifies the FacesServlet mapping in the current request map.
*/
@@ -45,7 +53,16 @@
DefaultResourceHandlerSupport.class.getName() + ".CACHED_SERVLET_MAPPING";
private ResourceLoader[] _resourceLoaders;
+
+ private Long _startupTime;
+
+ private Long _maxTimeExpires;
+ public DefaultResourceHandlerSupport()
+ {
+ _startupTime = System.currentTimeMillis();
+ }
+
public String calculateResourceBasePath(FacesContext facesContext)
{
FacesServletMapping mapping = getFacesServletMapping(facesContext);
@@ -208,4 +225,33 @@
}
}
}
+
+ public long getStartupTime()
+ {
+ return _startupTime;
+ }
+
+ public long getMaxTimeExpires()
+ {
+ if (_maxTimeExpires == null)
+ {
+ String time = FacesContext.getCurrentInstance().getExternalContext().getInitParameter(RESOURCE_MAX_TIME_EXPIRES);
+ if (time != null && time.length() > 0)
+ {
+ try
+ {
+ _maxTimeExpires = Long.parseLong(time);
+ }
+ catch (NumberFormatException e)
+ {
+ _maxTimeExpires = 604800000L;
+ }
+ }
+ else
+ {
+ _maxTimeExpires = 604800000L;
+ }
+ }
+ return _maxTimeExpires;
+ }
}
Modified: myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/ResourceHandlerSupport.java
URL: http://svn.apache.org/viewvc/myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/ResourceHandlerSupport.java?rev=891494&r1=891493&r2=891494&view=diff
==============================================================================
--- myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/ResourceHandlerSupport.java
(original)
+++ myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/application/ResourceHandlerSupport.java
Thu Dec 17 00:42:18 2009
@@ -71,4 +71,18 @@
*/
String getMapping();
+ /**
+ * Return the time when the app started. This is useful to set the
+ * "Last-Modified" header in some specific cases.
+ *
+ * @return
+ */
+ long getStartupTime();
+
+ /**
+ * Return the time that should be set on "Expires" header in a resource.
+ *
+ * @return
+ */
+ long getMaxTimeExpires();
}
Modified: myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceImpl.java
URL: http://svn.apache.org/viewvc/myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceImpl.java?rev=891494&r1=891493&r2=891494&view=diff
==============================================================================
--- myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceImpl.java (original)
+++ myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceImpl.java Thu
Dec 17 00:42:18 2009
@@ -22,13 +22,21 @@
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.net.URL;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.TimeZone;
import javax.el.ELContext;
import javax.el.ValueExpression;
+import javax.faces.application.ProjectStage;
import javax.faces.application.Resource;
import javax.faces.application.ResourceHandler;
import javax.faces.context.FacesContext;
@@ -82,9 +90,9 @@
{
String contentType = getContentType();
- return ("text/css".equals(contentType) ||
+ return ("text/css".equals(contentType)/* ||
"text/javascript".equals(contentType) ||
- "application/x-javascript".equals(contentType) );
+ "application/x-javascript".equals(contentType)*/ );
}
private class ValueExpressionFilterInputStream extends InputStream
@@ -211,9 +219,44 @@
@Override
public Map<String, String> getResponseHeaders()
{
- // TODO: Read the HTTP documentation to see how we can enhance this
- //part. For now, use always an empty map.
- return Collections.emptyMap();
+ FacesContext facesContext = FacesContext.getCurrentInstance();
+
+ if (facesContext.getApplication().getResourceHandler().isResourceRequest(facesContext))
+ {
+ Map<String, String> headers = new HashMap<String, String>();
+
+ long lastModified;
+ try
+ {
+ lastModified = ResourceUtils.getResourceLastModified(this.getURL());
+ }
+ catch (IOException e)
+ {
+ lastModified = -1;
+ }
+
+ // Here we have two cases: If the file could contain EL Expressions
+ // the last modified time is the greatest value between application startup and
+ // the value from file.
+ if (this.couldResourceContainValueExpressions() &&
+ lastModified < _resourceHandlerSupport.getStartupTime())
+ {
+ lastModified = _resourceHandlerSupport.getStartupTime();
+ }
+
+ if (lastModified >= 0)
+ {
+ headers.put("Last-Modified", ResourceUtils.formatDateHeader(lastModified));
+ headers.put("Expires", ResourceUtils.formatDateHeader(System.currentTimeMillis()+_resourceHandlerSupport.getMaxTimeExpires()));
+ }
+
+ return headers;
+ }
+ else
+ {
+ //No need to return headers
+ return Collections.emptyMap();
+ }
}
@Override
@@ -225,8 +268,61 @@
@Override
public boolean userAgentNeedsUpdate(FacesContext context)
{
- // TODO: When and How we can return safely false?
+ // RFC2616 says related to If-Modified-Since header the following:
+ //
+ // "... The If-Modified-Since request-header field is used with a method to
+ // make it conditional: if the requested variant has not been modified since
+ // the time specified in this field, an entity will not be returned from
+ // the server; instead, a 304 (not modified) response will be returned
+ // without any message-body..."
+ //
+ // This method is called from ResourceHandlerImpl.handleResourceRequest and if
+ // returns false send a 304 Not Modified response.
+
+ String ifModifiedSinceString = context.getExternalContext().getRequestHeaderMap().get("If-Modified-Since");
+
+ if (ifModifiedSinceString == null)
+ {
+ return true;
+ }
+
+ Long ifModifiedSince = ResourceUtils.parseDateHeader(ifModifiedSinceString);
+
+ if (ifModifiedSince == null)
+ {
+ return true;
+ }
+
+ Long lastModified;
+ try
+ {
+ lastModified = ResourceUtils.getResourceLastModified(this.getURL());
+ }
+ catch (IOException exception)
+ {
+ lastModified = -1L;
+ }
+
+ if (lastModified >= 0)
+ {
+ if (this.couldResourceContainValueExpressions() &&
+ lastModified < _resourceHandlerSupport.getStartupTime())
+ {
+ lastModified = _resourceHandlerSupport.getStartupTime();
+ }
+
+ // If the lastModified date is lower or equal than ifModifiedSince,
+ // the agent does not need to update.
+ // Note the lastModified time is set at milisecond precision, but when
+ // the date is parsed and sent on ifModifiedSince, the exceding miliseconds
+ // are trimmed. So, we have to compare trimming this from the calculated
+ // lastModified time.
+ if ( (lastModified-(lastModified % 1000)) <= ifModifiedSince)
+ {
+ return false;
+ }
+ }
+
return true;
}
-
}
Added: myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceUtils.java
URL: http://svn.apache.org/viewvc/myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceUtils.java?rev=891494&view=auto
==============================================================================
--- myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceUtils.java (added)
+++ myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/resource/ResourceUtils.java Thu
Dec 17 00:42:18 2009
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class ResourceUtils
+{
+ // TODO: In tomcat and jetty it is implemented a Flyweight pattern when converting
+ // date headers. For now it is better keep this stuff simple.
+ private static final String HTTP_RESPONSE_DATE_HEADER =
+ "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+ private static final String[] HTTP_REQUEST_DATE_HEADER = {
+ "EEE, dd MMM yyyy HH:mm:ss zzz", "EEEEEE, dd-MMM-yy HH:mm:ss zzz",
+ "EEE MMMM d HH:mm:ss yyyy" };
+
+ private static TimeZone __GMT = TimeZone.getTimeZone("GMT");
+
+ public static String formatDateHeader(long value)
+ {
+ SimpleDateFormat format = new SimpleDateFormat(
+ HTTP_RESPONSE_DATE_HEADER,
+ Locale.US);
+ format.setTimeZone(__GMT);
+ return format.format(new Date(value));
+ }
+
+ public static Long parseDateHeader(String value)
+ {
+ Date date = null;
+ for (int i = 0; (date == null) && (i < HTTP_REQUEST_DATE_HEADER.length);
i++)
+ {
+ try
+ {
+ SimpleDateFormat format = new SimpleDateFormat(
+ HTTP_REQUEST_DATE_HEADER[i], Locale.US);
+ format.setTimeZone(__GMT);
+ date = format.parse(value);
+ }
+ catch (ParseException e)
+ {
+ ;
+ }
+ }
+ if (date == null)
+ {
+ return null;
+ }
+ return new Long(date.getTime());
+ }
+
+ //Taken from trinidad URLUtils
+ public static long getResourceLastModified(URL url) throws IOException
+ {
+ if ("file".equals(url.getProtocol()))
+ {
+ String externalForm = url.toExternalForm();
+ // Remove the "file:"
+ File file = new File(externalForm.substring(5));
+
+ return file.lastModified();
+ }
+ else
+ {
+ return getResourceLastModified(url.openConnection());
+ }
+ }
+
+ //Taken from trinidad URLUtils
+ public static long getResourceLastModified(URLConnection connection) throws IOException
+ {
+ long modified;
+ if (connection instanceof JarURLConnection)
+ {
+ // The following hack is required to work-around a JDK bug.
+ // getLastModified() on a JAR entry URL delegates to the actual JAR file
+ // rather than the JAR entry.
+ // This opens internally, and does not close, an input stream to the JAR
+ // file.
+ // In turn, you cannot close it by yourself, because it's internal.
+ // The work-around is to get the modification date of the JAR file
+ // manually,
+ // and then close that connection again.
+
+ URL jarFileUrl = ((JarURLConnection) connection).getJarFileURL();
+ URLConnection jarFileConnection = jarFileUrl.openConnection();
+
+ try
+ {
+ modified = jarFileConnection.getLastModified();
+ }
+ finally
+ {
+ try
+ {
+ jarFileConnection.getInputStream().close();
+ }
+ catch (Exception exception)
+ {
+ // Ignored
+ }
+ }
+ }
+ else
+ {
+ modified = connection.getLastModified();
+ }
+
+ return modified;
+ }
+}
|