package er.extensions.appserver; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import org.apache.commons.lang3.CharEncoding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResourceManager; import com.webobjects.appserver._private.WODeployedBundle; import com.webobjects.appserver._private.WOProjectBundle; import com.webobjects.appserver._private.WOURLEncoder; import com.webobjects.appserver._private.WOURLValuedElementData; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSLog; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSPathUtilities; import com.webobjects.foundation._NSStringUtilities; import com.webobjects.foundation._NSThreadsafeMutableDictionary; import er.extensions.foundation.ERXDictionaryUtilities; import er.extensions.foundation.ERXFileUtilities; import er.extensions.foundation.ERXMutableURL; import er.extensions.foundation.ERXProperties; /** * Replacement of the WOResourceManager which adds: * <ul> * <li> dealing with nested web server resources when not deploying * <li> resource versioning (for better caching control) * </ul> * * @property er.extensions.ERXResourceManager.versionManager the class name of the version manager to use (or "default", or "properties") * @author ak * @author mschrag */ public class ERXResourceManager extends WOResourceManager { private static Logger log = LoggerFactory.getLogger(ERXResourceManager.class); private WODeployedBundle TheAppProjectBundle; private _NSThreadsafeMutableDictionary<String, WOURLValuedElementData> _urlValuedElementsData; private IVersionManager _versionManager; private final _NSThreadsafeMutableDictionary _myFrameworkProjectBundles = new _NSThreadsafeMutableDictionary(new NSMutableDictionary(128)); private static final NSDictionary<String, String> _mimeTypes = _additionalMimeTypes(); protected ERXResourceManager() { TheAppProjectBundle = _initAppBundle(); _initFrameworkProjectBundles(); try { Field field = WOResourceManager.class.getDeclaredField("_urlValuedElementsData"); field.setAccessible(true); // AK: yeah, hack, I know... _urlValuedElementsData = (_NSThreadsafeMutableDictionary) field.get(this); } catch (java.lang.SecurityException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (NoSuchFieldException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (IllegalArgumentException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (IllegalAccessException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } String versionManagerClassName = ERXProperties.stringForKeyWithDefault("er.extensions.ERXResourceManager.versionManager", "default"); if ("default".equals(versionManagerClassName)) { _versionManager = new DefaultVersionManager(); } else if ("properties".equals(versionManagerClassName)) { _versionManager = new PropertiesVersionManager(); } else { try { _versionManager = Class.forName(versionManagerClassName).asSubclass(IVersionManager.class).newInstance(); } catch (java.lang.InstantiationException e) { throw new RuntimeException("Unable to create the specified version manager '" + versionManagerClassName + ".", e); } catch (java.lang.IllegalAccessException e) { throw new RuntimeException("Unable to create the specified version manager '" + versionManagerClassName + ".", e); } catch (ClassNotFoundException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } } /** * Sets the version manager to use for this resource manager. * * @param versionManager the version manager to use for this resource manager */ public void setVersionManager(IVersionManager versionManager) { _versionManager = versionManager; } /** * @return the current version manager for this resource manager. */ public IVersionManager versionManager() { return _versionManager; } private void _initFrameworkProjectBundles() { NSBundle aBundle = null; NSArray aFrameworkBundleList = NSBundle.frameworkBundles(); for(Enumeration aBundleEnumerator = aFrameworkBundleList.objectEnumerator(); aBundleEnumerator.hasMoreElements(); _erxCachedBundleForFrameworkNamed(aBundle.name())) aBundle = (NSBundle)aBundleEnumerator.nextElement(); } private static WODeployedBundle _initAppBundle() { Object obj = null; try { WODeployedBundle wodeployedbundle = WODeployedBundle.deployedBundle(); obj = wodeployedbundle.projectBundle(); if (obj != null) { log.warn("Application project found: Will locate resources in '{}' rather than '{}'.", ((WOProjectBundle) obj).projectPath(), wodeployedbundle.bundlePath()); } else { obj = wodeployedbundle; } } catch (Exception exception) { log.error("<WOResourceManager> Unable to initialize AppProjectBundle for reason:", exception); throw NSForwardException._runtimeExceptionForThrowable(exception); } return (WODeployedBundle) obj; } private static WODeployedBundle _locateBundleForFrameworkNamed(String aFrameworkName) { WODeployedBundle aBundle = null; aBundle = ERXDeployedBundle.deployedBundleForFrameworkNamed(aFrameworkName); if(aBundle == null) { NSBundle nsBundle = NSBundle.bundleForName(aFrameworkName); if(nsBundle != null) aBundle = _bundleWithNSBundle(nsBundle); } return aBundle; } private static WODeployedBundle _bundleWithNSBundle(NSBundle nsBundle) { WODeployedBundle aBundle = null; WODeployedBundle aDeployedBundle = ERXDeployedBundle.bundleWithNSBundle(nsBundle); WODeployedBundle aProjectBundle = aDeployedBundle.projectBundle(); if(aProjectBundle != null) { if(WOApplication._isDebuggingEnabled()) NSLog.debug.appendln((new StringBuilder()).append("Framework project found: Will locate resources in '").append(aProjectBundle.bundlePath()).append("' rather than '").append(aDeployedBundle.bundlePath()).append("' .").toString()); aBundle = aProjectBundle; } else { aBundle = aDeployedBundle; } return aBundle; } public NSArray _frameworkProjectBundles() { return _myFrameworkProjectBundles.immutableClone().allValues(); } public WODeployedBundle _erxCachedBundleForFrameworkNamed(String aFrameworkName) { WODeployedBundle aBundle = null; if(aFrameworkName != null) { aBundle = (WODeployedBundle)_myFrameworkProjectBundles.objectForKey(aFrameworkName); if(aBundle == null) { aBundle = _locateBundleForFrameworkNamed(aFrameworkName); if(aBundle != null) _myFrameworkProjectBundles.setObjectForKey(aBundle, aFrameworkName); } } if(aBundle == null) aBundle = TheAppProjectBundle; return aBundle; } private String _cachedURLForResource(String name, String bundleName, NSArray languages, WORequest request) { String result = null; if (bundleName != null) { WODeployedBundle wodeployedbundle = _erxCachedBundleForFrameworkNamed(bundleName); if (wodeployedbundle != null) { result = wodeployedbundle.urlForResource(name, languages); } if (result == null) { result = "/ERROR/NOT_FOUND/framework=" + bundleName + "/filename=" + (name == null ? "*null*" : name); } } else { result = TheAppProjectBundle.urlForResource(name, languages); if (result == null) { String appName = WOApplication.application().name(); result = "/ERROR/NOT_FOUND/app=" + appName + "/filename=" + (name == null ? "*null*" : name); } } String resourceUrlPrefix = null; if (ERXRequest.isRequestSecure(request)) { resourceUrlPrefix = ERXProperties.stringForKey("er.extensions.ERXResourceManager.secureResourceUrlPrefix"); } else { resourceUrlPrefix = ERXProperties.stringForKey("er.extensions.ERXResourceManager.resourceUrlPrefix"); } if (resourceUrlPrefix != null && resourceUrlPrefix.length() > 0) { result = resourceUrlPrefix + result; } return result; } @Override public String urlForResourceNamed(String name, String bundleName, NSArray<String> languages, WORequest request) { String completeURL = null; if (request == null || request.isUsingWebServer() && !WOApplication.application()._rapidTurnaroundActiveForAnyProject()) { completeURL = _cachedURLForResource(name, bundleName, languages, request); } else { URL url = pathURLForResourceNamed(name, bundleName, languages); String fileURL = null; if (url == null) { fileURL = "ERROR_NOT_FOUND_framework_" + (bundleName == null ? "*null*" : bundleName) + "_filename_" + (name == null ? "*null*" : name); } else { fileURL = url.toString(); cacheDataIfNotInCache(fileURL); } String encoded = WOURLEncoder.encode(fileURL); String key = WOApplication.application().resourceRequestHandlerKey(); if (WOApplication.application()._rapidTurnaroundActiveForAnyProject() && WOApplication.application().isDirectConnectEnabled()) { key = "_wr_"; } WOContext context = (WOContext) request.valueForKey("context"); String wodata = _NSStringUtilities.concat("wodata", "=", encoded); if (context != null) { completeURL = context.urlWithRequestHandlerKey(key, null, wodata); } else { StringBuilder sb = new StringBuilder(request.applicationURLPrefix()); sb.append('/'); sb.append(key); sb.append('?'); sb.append(wodata); completeURL = sb.toString(); } // AK: TODO get rid of regex int offset = completeURL.indexOf("?wodata=file%3A"); if (offset >= 0) { completeURL = completeURL.replaceFirst("\\?wodata=file%3A", "/wodata="); if (completeURL.indexOf("/wodata=") > 0) { completeURL = completeURL.replaceAll("%2F", "/"); // SWK: On Windows we have /C%3A/ changed to /C: completeURL = completeURL.replaceAll("%3A", ":"); } } } completeURL = _versionManager.versionedUrlForResourceNamed(completeURL, name, bundleName, languages, request); completeURL = _postprocessURL(completeURL, bundleName); return completeURL; } protected String _postprocessURL(String url, String bundleName) { if (WOApplication.application() instanceof ERXApplication) { WODeployedBundle bundle = _cachedBundleForFrameworkNamed(bundleName); return ERXApplication.erxApplication()._rewriteResourceURL(url, bundle); } return url; } private WOURLValuedElementData cachedDataForKey(String key) { WOURLValuedElementData data = _urlValuedElementsData.objectForKey(key); if (data == null && key != null && key.startsWith("file:") && ERXApplication.isDevelopmentModeSafe()) { data = cacheDataIfNotInCache(key); } return data; } protected WOURLValuedElementData cacheDataIfNotInCache(String key) { WOURLValuedElementData data = _urlValuedElementsData.objectForKey(key); if (data == null) { String contentType = contentTypeForResourceNamed(key); data = new WOURLValuedElementData(null, contentType, key); _urlValuedElementsData.setObjectForKey(data, key); } return data; } @Override public WOURLValuedElementData _cachedDataForKey(String key) { WOURLValuedElementData wourlvaluedelementdata = null; if (key != null) { wourlvaluedelementdata = cachedDataForKey(key); } return wourlvaluedelementdata; } /** * Overrides the original implementation appending the additionalMimeTypes to the content types dictionary. * * @return a dictionary containing the original mime types supported along with the additional mime types * contributed by this class. * @see com.webobjects.appserver.WOResourceManager#_contentTypesDictionary() */ @Override public NSDictionary _contentTypesDictionary() { return ERXDictionaryUtilities.dictionaryWithDictionaryAndDictionary(_mimeTypes, super._contentTypesDictionary()); } /** * Returns whether or not complete resource URLs should be generated. * @param context the context * @return whether or not complete resource URLs should be generated */ public static boolean _shouldGenerateCompleteResourceURL(WOContext context) { return context instanceof ERXWOContext && ((ERXWOContext)context)._generatingCompleteResourceURLs() && !ERXApplication.erxApplication().rewriteDirectConnectURL(); } /** * Returns a fully qualified URL for the given partial resource URL (i.e. turns /whatever into http://server/whatever). * @param url the partial resource URL * @param secure whether or not to generate a secure URL * @param context the current context * @return the complete URL */ public static String _completeURLForResource(String url, Boolean secure, WOContext context) { String completeUrl; boolean requestIsSecure = ERXRequest.isRequestSecure(context.request()); boolean resourceIsSecure = (secure == null) ? requestIsSecure : secure.booleanValue(); if ((resourceIsSecure && ERXProperties.stringForKey("er.extensions.ERXResourceManager.secureResourceUrlPrefix") == null) || (!resourceIsSecure && ERXProperties.stringForKey("er.extensions.ERXResourceManager.resourceUrlPrefix") == null)) { StringBuffer sb = new StringBuffer(); String serverPortStr = context.request()._serverPort(); int serverPort = (serverPortStr == null) ? 0 : Integer.parseInt(serverPortStr); context.request()._completeURLPrefix(sb, resourceIsSecure, serverPort); sb.append(url); completeUrl = sb.toString(); } else { completeUrl = url; } return completeUrl; } /** * IVersionManager provides an interface for adding version numbers to * WebServerResources. This allows you to turn on "infinite" expiration * dates in mod_expires, and instead control reloading by changing the * resource's URL. As an example, you might append a version number as a * query string on the URL (whatever.gif?1). * * @author mschrag */ public static interface IVersionManager { /** * Returns the variant of the given resource URL adjusted to include * version information. * * @param resourceUrl * the original resource URL * @param name * the name of the resource being loaded * @param bundleName * the name of the bundle that contains the resource * @param languages * the languages requested * @param request * the request * @return a versioned variant of the resourceUrl */ public String versionedUrlForResourceNamed(String resourceUrl, String name, String bundleName, NSArray<String> languages, WORequest request); } /** * DefaultVersionManager just returns the resourceUrl unmodified. * * @author mschrag */ public static class DefaultVersionManager implements IVersionManager { /** * @return resourceUrl */ public String versionedUrlForResourceNamed(String resourceUrl, String name, String bundleName, NSArray<String> languages, WORequest request) { return resourceUrl; } } /** * Implementation of the IVersionManager interface which provides the * ability to control resource version numbers with Properties settings, * and appends the query parameter "?xxx" to WebServerResource URLs. * * @property er.extensions.ERXResourceManager.versionManager.default the * default version to use when an explicit version is not * specified, defaults to app startup time. Ideally you should set * this explicitly when you deploy, or multiple instance * deployments will end up with different version numbers for the * same resource. * @property er.extensions.ERXResourceManager.versionManager.[bundleName].[resourceName] * the version to send for the specified resource. If not set * explicitly, the app default version will be used instead. * @author mschrag */ public static class PropertiesVersionManager implements IVersionManager { private static Logger log = LoggerFactory.getLogger(ERXResourceManager.class); private String _defaultVersion; public PropertiesVersionManager() { String key = "er.extensions.ERXResourceManager.versionManager.default"; _defaultVersion = ERXProperties.stringForKey(key); if (_defaultVersion == null) { _defaultVersion = String.valueOf(System.currentTimeMillis()); } } public String versionedUrlForResourceNamed(String resourceUrl, String name, String bundleName, NSArray<String> languages, WORequest request) { if (bundleName == null) { bundleName = "app"; } String key = "er.extensions.ERXResourceManager.versionManager." + bundleName + "." + name; String version = ERXProperties.stringForKey(key); if (version == null) { version = _defaultVersion; } else if ("none".equals(version) || version.length() == 0) { version = null; } if (version != null) { try { ERXMutableURL url = new ERXMutableURL(resourceUrl); url.addQueryParameter("", version); resourceUrl = url.toExternalForm(); } catch (MalformedURLException e) { log.error("Failed to construct URL from '{}'.", resourceUrl, e); } } return resourceUrl; } } /** * Overridden to supply additional mime types that are not present in the * JavaWebObjects framework. * @param aResourcePath file path of the resource, or just file name of the resource, * as only the extension is required * @return HTTP content type for the named resource specified by <code>aResourcePath</code> */ @Override public String contentTypeForResourceNamed(String aResourcePath) { String aPathExtension = NSPathUtilities.pathExtension(aResourcePath); if(aPathExtension != null && aPathExtension.length() != 0) { String mime = _mimeTypes.objectForKey(aPathExtension.toLowerCase()); if(mime != null) { return mime; } } return super.contentTypeForResourceNamed(aResourcePath); } private static NSDictionary<String, String> _additionalMimeTypes() { NSDictionary<String, String> plist = (NSDictionary<String, String>)ERXFileUtilities.readPropertyListFromFileInFramework("AdditionalMimeTypes.plist", "ERExtensions", null, CharEncoding.UTF_8); return plist; } }