/*******************************************************************************
* Copyright (c) 2002, 2009 Innoopract Informationssysteme GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Innoopract Informationssysteme GmbH - initial API and implementation
* EclipseSource - ongoing development
******************************************************************************/
package org.eclipse.rwt.internal.resources;
import java.io.*;
import java.net.*;
import java.text.MessageFormat;
import java.util.*;
import org.eclipse.rwt.Adaptable;
import org.eclipse.rwt.internal.ConfigurationReader;
import org.eclipse.rwt.internal.IEngineConfig;
import org.eclipse.rwt.internal.service.ContextProvider;
import org.eclipse.rwt.internal.util.*;
import org.eclipse.rwt.resources.IResourceManager;
/** <p>The resource manager is responsible for registering resources
* like images, css files etc. which are available on the applications
* classpath. The registered files will be read out from their libraries
* and delivered if requested. Usually resources are stored in libraries
* in the WEB-INF/lib directory of a web-application</p>
* <p>Implementation as Singleton.</p>
* <p>This class is not intended to be used by clients.</p>
*/
public class ResourceManagerImpl
extends ResourceBase
implements IResourceManager, Adaptable
{
public static final String RESOURCES = "rwt-resources";
final static String RESOURCE = "w4t_resource";
final static String RESOURCE_VERSION = "w4t_res_version";
/** <p>The singleton instance of ResourceManager.</p> */
private static IResourceManager _instance;
private final String webAppRoot;
private final Map repository;
private final Map cache;
private ClassLoader loader;
private ThreadLocal contextLoader;
private JsConcatenator jsConcatenator;
private static final class Resource {
/** the 'raw' content of the resource. In case of a text resource (charset
* was given) the content is UTF-8 encoded. */
private final int[] content;
/** the charset in which the resource was encoded before red or null for
* binary resources. */
private final String charset;
/** the reources' version or null for 'no version' */
private final Integer version;
public Resource( final int[] content,
final String charset,
final Integer version )
{
this.charset = charset;
this.content = content;
this.version = version;
}
public String getCharset() {
return charset;
}
public int[] getContent() {
return content;
}
public Integer getVersion() {
return version;
}
}
private ResourceManagerImpl( final String webAppRoot ) {
this.webAppRoot = webAppRoot;
repository = new Hashtable();
cache = new Hashtable();
contextLoader = new ThreadLocal();
}
/** <p>Returns the singleton instance of ResourceManager.</p> */
public static synchronized IResourceManager createInstance(
final String webAppRoot,
final String mode )
{
if( _instance == null ) {
_instance = new ResourceManagerImpl( webAppRoot );
setDeliveryMode( mode );
}
return _instance;
}
/** <p>Retruns the singleton instance of <code>IResourceManager</code>.</p> */
public static IResourceManager getInstance() {
return _instance;
}
/**
* <p>Loads the given <code>resource</code> from the class path.</p>
* @param resource the name of the resource to be loaded. Must not be
* <code>null</code>
*/
public static String load( final String resource ) {
ParamCheck.notNull( resource, "resource" );
return ( ( ResourceManagerImpl )_instance ).doLoad( resource );
}
/**
* <p>Returns the content of the resource denoted by <code>name</code>.</p>
* @param name the name of the resource to find, must not be
* <code>null</code>.
* @param version the version (can be obtained by {@link #findVersion(String)}
* <code>findVersion(String)</code>) of the resource or <code>null</code>
* if the resource is unversioned.
* @return the content of the resource or <code>null</code> if no resource
* with the given <code>name</code> and <code>version</code> exists.
*/
public static int[] findResource( final String name, final Integer version ) {
ParamCheck.notNull( name, "name" );
int[] result = null;
ResourceManagerImpl manager = ( ResourceManagerImpl )_instance;
Resource resource = ( Resource )manager.cache.get( createKey( name ) );
if( resource != null ) {
if( ( version == null && resource.getVersion() == null )
|| ( version != null && version.equals( resource.getVersion() ) ) )
{
result = resource.getContent();
}
}
return result;
}
/**
* <p>Returns the version number for the previously {@link
* #register(String, String, RegisterOptions) registered} resource.</p>
* @param name the name of the resource for which the version number should be
* obtained. Must not be <code>null</code>.
* @return the version number or <code>null</code> if either no such resource
* was registered or the resource does not have a version number.
* @throws NullPointerException when <<code>name</code> is <code>null</code>.
*/
public static Integer findVersion( final String name ) {
ParamCheck.notNull( name, "name" );
Integer result = null;
ResourceManagerImpl manager = ( ResourceManagerImpl )_instance;
Resource resource = ( Resource )manager.cache.get( createKey( name ) );
if( resource != null ) {
result = resource.getVersion();
}
return result;
}
//////////////////////
// interface Adaptable
public Object getAdapter( final Class adapter ) {
Object result = null;
if( adapter == JsConcatenator.class ){
if( jsConcatenator == null ) {
jsConcatenator = new JsConcatenator() {
private String content;
private boolean registered = false;
public String getLocation() {
String concatedName = "rap.js";
if( !registered ) {
byte[] content = getContent().getBytes();
register( concatedName,
new ByteArrayInputStream( content ),
HTML.CHARSET_NAME_UTF_8,
RegisterOptions.VERSION );
registered = true;
}
return ResourceManagerImpl.this.getLocation( concatedName );
}
public void startJsConcatenation() {
ResourceUtil.startJsConcatenation();
}
public String getContent() {
if( content == null ) {
content = ResourceUtil.getJsConcatenationContentAsString();
}
return content;
}
};
}
result = jsConcatenator;
}
return result;
}
/////////////////////////////
// interface IResourceManager
public void register( final String name ) {
ParamCheck.notNull( name, "name" );
doRegister( name, null, RegisterOptions.NONE );
}
public void register( final String name, final String charset )
{
ParamCheck.notNull( name, "name" );
ParamCheck.notNull( charset, "charset" );
doRegister( name, charset, RegisterOptions.NONE );
}
public void register( final String name,
final String charset,
final RegisterOptions options )
{
ParamCheck.notNull( name, "name" );
ParamCheck.notNull( charset, "charset" );
ParamCheck.notNull( options, "options" );
doRegister( name, charset, options );
}
public void register( final String name, final InputStream is ) {
ParamCheck.notNull( name, "name" );
ParamCheck.notNull( is, "is" );
String key = createKey( name );
try {
int[] content = ResourceUtil.readBinary( is );
doRegister( name, null, RegisterOptions.NONE, key, content );
} catch ( IOException e ) {
String text = "Failed to register resource ''{0}''.";
String msg = MessageFormat.format( text, new Object[] { name } );
throw new ResourceRegistrationException( msg, e ) ;
}
repository.put( key, name );
}
public void register( final String name,
final InputStream is,
final String charset,
final RegisterOptions options )
{
ParamCheck.notNull( name, "name" );
ParamCheck.notNull( is, "is" );
ParamCheck.notNull( charset, "charset" );
ParamCheck.notNull( options, "options" );
boolean compress = shouldCompress( options );
String key = createKey( name );
try {
int[] content = ResourceUtil.read( is, charset, compress );
doRegister( name, charset, options, key, content );
} catch ( IOException e ) {
String text = "Failed to register resource ''{0}''.";
String msg = MessageFormat.format( text, new Object[] { name } );
throw new ResourceRegistrationException( msg, e ) ;
}
repository.put( key, name );
}
public boolean unregister( final String name ) {
ParamCheck.notNull( name, "name" );
boolean result = false;
String key = createKey( name );
String fileName = ( String )repository.remove( key );
if( fileName != null ) {
result = true;
Integer version = findVersion( name );
File file = getDiskLocation( name, version );
file.delete();
cache.remove( key );
}
return result;
}
public String getCharset( final String name ) {
ParamCheck.notNull( name, "name" );
Resource resource = ( Resource )cache.get( createKey( name ) );
return resource.getCharset();
}
public boolean isRegistered( final String name ) {
ParamCheck.notNull( name, "name" );
String key = createKey( name );
String fileName = ( String )repository.get( key );
return fileName != null;
}
public String getLocation( final String name ) {
ParamCheck.notNull( name, "name" );
String key = createKey( name );
String fileName = ( String )repository.get( key );
return createRequestURL( fileName, findVersion( name ) );
}
public URL getResource( final String name ) {
return getLoader().getResource( name );
}
public InputStream getResourceAsStream( final String name ) {
URL resource = getLoader().getResource( name );
InputStream result = null;
if( resource != null ) {
try {
URLConnection connection = resource.openConnection();
connection.setUseCaches( false );
result = connection.getInputStream();
} catch( final IOException ignore ) {
// ignore
}
}
return result;
}
public Enumeration getResources( final String name ) throws IOException {
return getLoader().getResources( name );
}
public void setContextLoader( final ClassLoader classLoader ) {
contextLoader.set( classLoader );
}
public ClassLoader getContextLoader() {
return ( ClassLoader )contextLoader.get();
}
public InputStream getRegisteredContent( final String name ) {
InputStream result = null;
String key = createKey( name );
String fileName = ( String )repository.get( key );
if( fileName != null ) {
// TODO [rst] Works only for non-versioned content for now
File file = getDiskLocation( name, null );
try {
result = new FileInputStream( file );
} catch( FileNotFoundException e ) {
// should not happen
throw new RuntimeException( e );
}
}
return result;
}
//////////////////
// helping methods
private ClassLoader getLoader() {
ClassLoader result = loader;
if( getContextLoader() != null
&& getContextLoader() != ResourceManagerImpl.class.getClassLoader() )
{
result = getContextLoader();
} else if( loader == null ) {
IEngineConfig engineConfig = ConfigurationReader.getEngineConfig();
List buffer = WebAppURLs.getWebAppURLs( engineConfig );
URL[] urls = new URL[ buffer.size() ];
buffer.toArray( urls );
ClassLoader parent = getClass().getClassLoader();
loader = new URLClassLoader( urls, parent );
result = loader;
}
return result;
}
private static String createKey( final String name ) {
return String.valueOf( name.hashCode() );
}
private static String createRequestURL( final String fileName,
final Integer version )
{
String result;
String newFileName = fileName.replace( '\\', '/' );
if( isDeliveryMode( DELIVER_FROM_DISK ) ) {
StringBuffer url = new StringBuffer();
url.append( RESOURCES );
url.append( "/" );
String escapedFilename = escapeFilename( newFileName );
url.append( versionedResourceName( escapedFilename, version ) );
result = url.toString();
} else {
StringBuffer url = new StringBuffer();
url.append( URLHelper.getURLString( false ) );
URLHelper.appendFirstParam( url, RESOURCE, newFileName );
if( version != null ) {
URLHelper.appendParam( url,
RESOURCE_VERSION,
String.valueOf( version.intValue() ) );
}
result = ContextProvider.getResponse().encodeURL( url.toString() );
}
return result;
}
private String doLoad( final String resource ) {
String key = createKey( resource );
if( !repository.containsKey( key ) ) {
try {
register( resource );
} catch( ResourceRegistrationException e ) {
// application file which is not managed by the resource manager
repository.put( key, resource );
}
}
return createRequestURL( resource, null );
}
private void doRegister( final String name,
final String charset,
final RegisterOptions options )
{
String key = createKey( name );
// TODO [rh] should throw exception if contains key but has different
// charset or options
if( !repository.containsKey( key ) ) {
boolean compress = shouldCompress( options );
try {
int[] content = ResourceUtil.read( name, charset, compress );
doRegister( name, charset, options, key, content );
} catch ( IOException e ) {
String text = "Failed to register resource ''{0}''.";
String msg = MessageFormat.format( text, new Object[] { name } );
throw new ResourceRegistrationException( msg, e ) ;
}
repository.put( key, name );
}
}
private void doRegister( final String name,
final String charset,
final RegisterOptions options,
final String key,
final int[] content )
throws IOException
{
Integer version = computeVersion( content, options );
if( isDeliveryMode( DELIVER_FROM_DISK ) ) {
File location = getDiskLocation( name, version );
createFile( location );
ResourceUtil.write( location, content );
cache.put( key, new Resource( null, charset, version ) );
} else if( isDeliveryMode( DELIVER_BY_SERVLET ) ) {
cache.put( key, new Resource( content, charset, version ) );
} else if( isDeliveryMode( DELIVER_BY_SERVLET_AND_TEMP_DIR ) ) {
File location = getTempLocation( name, version );
createFile( location );
ResourceUtil.write( location, content );
cache.put( key, new Resource( content, charset, version ) );
}
}
private static void createFile( final File fileToWrite ) throws IOException {
File dir = new File( fileToWrite.getParent() );
if( !dir.exists() ) {
if( !dir.mkdirs() ) {
throw new IOException( "Could not create directory structure: " + dir );
}
}
if( !fileToWrite.exists() ) {
fileToWrite.createNewFile();
}
}
private static Integer computeVersion( final int[] content,
final RegisterOptions options )
{
Integer result = null;
if( content != null && shouldVersion( options ) ) {
int version = 0;
for( int i = 0; i < content.length; i++ ) {
version = version * 31 + content[ i ];
}
result = new Integer( version );
}
return result;
}
private static boolean shouldVersion( final RegisterOptions options ) {
return ( options == RegisterOptions.VERSION
|| options == RegisterOptions.VERSION_AND_COMPRESS )
&& SystemProps.useVersionedJavaScript();
}
static String versionedResourceName( final String name,
final Integer version )
{
String result = name;
if( version != null ) {
String versionString = String.valueOf( version.intValue() );
int dotPos = name.lastIndexOf( '.' );
// ensure that the dot was found in name part (not path)
if( name.replace( '\\', '/' ).lastIndexOf( "/" ) > dotPos ) {
dotPos = -1;
}
if( dotPos == -1 ) {
// append version number if not suffix
result = name + versionString;
} else {
// insert version number between name and suffix
result = name.substring( 0, dotPos )
+ versionString
+ name.substring( dotPos );
}
}
return result;
}
private static boolean shouldCompress( final RegisterOptions options ) {
return ( options == RegisterOptions.COMPRESS
|| options == RegisterOptions.VERSION_AND_COMPRESS )
&& SystemProps.useCompressedJavaScript();
}
private File getDiskLocation( final String name, final Integer version ) {
StringBuffer filename = new StringBuffer();
filename.append( webAppRoot );
filename.append( File.separator );
filename.append( RESOURCES );
filename.append( File.separator );
filename.append( versionedResourceName( escapeFilename( name ), version ) );
return new File( filename.toString() );
}
private static String escapeFilename( final String name ) {
String result = name;
result = name.replaceAll( "\\$", "\\$\\$" );
result = result.replaceAll( ":", "\\$1" );
result = result.replaceAll( "\\?", "\\$2" );
return result;
}
private static File getTempLocation( final String name,
final Integer version )
{
StringBuffer result = new StringBuffer();
result.append( System.getProperty( "java.io.tmpdir" ) );
result.append( File.separator );
result.append( System.getProperty( "user.name" ) );
result.append( File.separator );
result.append( "w4toolkit" );
result.append( File.separator );
result.append( versionedResourceName ( name, version ) );
return new File( result.toString() );
}
}