/**
* Copyright@2011 wro4j
*/
package ro.isdc.wro.model.transformer;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.locator.UriLocator;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.locator.wildcard.DefaultWildcardStreamLocator;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardExpanderHandlerAware;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardStreamLocator;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardUriLocatorSupport;
import ro.isdc.wro.util.Function;
import ro.isdc.wro.util.Transformer;
/**
* A decorator which looks up for resources containing wildcards and replaces them with the corresponding collection of
* specific resources. For example, a resource of this type:
* <p/>
* {@code
* <js>/path/to/**.js</js>
* }
* <p/>
* can be transformed into:
* <p/>
* {@code
* <js>/path/to/a1.js</js>
* } <br/>
* {@code
* <js>/path/to/a2.js</js>
* } <br/>
* {@code
* <js>/path/to/a3.js</js>
* } <br/>
* <p/>
* This model transformation is also known as wildcard expander, because it mutates the model after it is built by
* adding resources to the group which contains resources with wildcard uri.
*
* @author Alex Objelean
* @created 18 Jul 2011
* @since 1.4.0
*/
@SuppressWarnings("serial")
public class WildcardExpanderModelTransformer
implements Transformer<WroModel> {
private static final Logger LOG = LoggerFactory.getLogger(WildcardExpanderModelTransformer.class);
@Inject
private UriLocatorFactory locatorFactory;
/**
* An instance of IOException having a special purpose: to skip subsequent attempts to localize a stream.
*/
public static class NoMoreAttemptsIOException
extends IOException {
public NoMoreAttemptsIOException(final String message) {
super(message);
}
}
/**
* {@inheritDoc}
*/
public synchronized WroModel transform(final WroModel input) {
final WroModel model = input;
for (final Group group : model.getGroups()) {
final List<Resource> resources = group.getResources();
for (final Resource resource : resources) {
processResource(group, resource);
}
}
LOG.debug("Transformed model: {}", model);
return model;
}
/**
* Process each resource and replace it with a collection of resources if it contains wildcard.
*/
private void processResource(final Group group, final Resource resource) {
final UriLocator uriLocator = locatorFactory.getInstance(resource.getUri());
if (uriLocator instanceof WildcardUriLocatorSupport) {
final WildcardStreamLocator wildcardStreamLocator = ((WildcardUriLocatorSupport) uriLocator).getWildcardStreamLocator();
// TODO should we probably handle the situation when wildcard is present, but the implementation is not
// expandedHandledAware?
if (wildcardStreamLocator.hasWildcard(resource.getUri())
&& wildcardStreamLocator instanceof WildcardExpanderHandlerAware) {
final WildcardExpanderHandlerAware expandedHandler = (WildcardExpanderHandlerAware) wildcardStreamLocator;
LOG.debug("Expanding resource: {}", resource.getUri());
final String baseNameFolder = computeBaseNameFolder(resource, uriLocator, expandedHandler);
LOG.debug("baseNameFolder: {}", baseNameFolder);
expandedHandler.setWildcardExpanderHandler(createExpanderHandler(group, resource, baseNameFolder));
try {
// trigger the wildcard replacement
uriLocator.locate(resource.getUri());
} catch (final IOException e) {
// log only
LOG.debug("[FAIL] problem while trying to expand wildcard for the following resource uri: {}",
resource.getUri());
} finally {
// remove the handler, it is not needed anymore
expandedHandler.setWildcardExpanderHandler(null);
}
}
}
}
/**
* Computes the file name of the folder where the resource is located. The implementation uses a trick by invoking the
* {@link WildcardExpanderHandlerAware} to get the baseName.
*/
private String computeBaseNameFolder(final Resource resource, final UriLocator uriLocator,
final WildcardExpanderHandlerAware expandedHandler) {
// Find the baseName
// add a recursive wildcard to trigger the wildcard detection. The simple wildcard ('*') is not enough because it
// won't work for folders containing only directories with no files.
LOG.debug("computeBaseNameFolder for resource {}", resource);
final String resourcePath = FilenameUtils.getFullPath(resource.getUri())
+ DefaultWildcardStreamLocator.RECURSIVE_WILDCARD;
LOG.debug("resourcePath: {}", resourcePath);
// use thread local because we need to assign a File inside an anonymous class and it fits perfectly
final ThreadLocal<String> baseNameFolderHolder = new ThreadLocal<String>();
expandedHandler.setWildcardExpanderHandler(createBaseNameComputerFunction(baseNameFolderHolder));
try {
uriLocator.locate(resourcePath);
} catch (final Exception e) {
LOG.debug("[FAIL] Exception caught during wildcard expanding for resource: {}\n with exception message {}",
resourcePath, e.getMessage());
}
if (baseNameFolderHolder.get() == null) {
LOG.debug("[FAIL] Cannot compute baseName folder for resource: {}", resource);
}
return baseNameFolderHolder.get();
}
private Function<Collection<File>, Void> createBaseNameComputerFunction(final ThreadLocal<String> baseNameFolderHolder) {
return new Function<Collection<File>, Void>() {
public Void apply(final Collection<File> input)
throws Exception {
LOG.debug("\texpanded Files: {}", input);
for (final File file : input) {
LOG.debug("\tsetting baseNameFolder: {}", file.getParent());
baseNameFolderHolder.set(file.getParent());
// no need to continue
break;
}
// use this to skip wildcard stream detection, we are only interested in the baseName
throw new NoMoreAttemptsIOException("BaseNameFolder computed successfully, skip further wildcard processing..");
}
};
}
/**
* create the handler which expand the resources containing wildcard.
*/
public Function<Collection<File>, Void> createExpanderHandler(final Group group, final Resource resource,
final String baseNameFolder) {
LOG.debug("createExpanderHandler using baseNameFolder: {}\n for resource {}", baseNameFolder, resource);
final Function<Collection<File>, Void> handler = new Function<Collection<File>, Void>() {
public Void apply(final Collection<File> files) {
if (baseNameFolder == null) {
// replacing group with empty list since the original uri has no associated resources.
// No BaseNameFolder found
LOG.warn("The resource {} is probably invalid, removing it from the group.", resource);
group.replace(resource, new ArrayList<Resource>());
} else {
final List<Resource> expandedResources = new ArrayList<Resource>();
LOG.debug("baseNameFolder: {}", baseNameFolder);
for (final File file : files) {
final String resourcePath = getFullPathNoEndSeparator(resource);
LOG.debug("\tresourcePath: {}", resourcePath);
LOG.debug("\tfile path: {}", file.getPath());
final String computedResourceUri = resourcePath
+ StringUtils.removeStart(file.getPath(), baseNameFolder).replace('\\', '/');
final Resource expandedResource = Resource.create(computedResourceUri, resource.getType());
LOG.debug("\texpanded resource: {}", expandedResource);
expandedResources.add(expandedResource);
}
LOG.debug("\treplace resource {}", resource);
group.replace(resource, expandedResources);
}
return null;
}
/**
* This method fixes the problem when a resource in a group uses deep wildcard and starts at the root.
* <p/>
* Find more details <a href="https://github.com/alexo/wro4j/pull/44">here</a>.
*/
private String getFullPathNoEndSeparator(final Resource resource) {
final String result = FilenameUtils.getFullPathNoEndSeparator(resource.getUri());
if (result != null && 1 == result.length() && 0 == FilenameUtils.indexOfLastSeparator(result)) {
return "";
}
return result;
}
};
return handler;
}
}