package ro.isdc.wro.maven.plugin.support;
import static org.apache.commons.lang3.Validate.notNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.maven.plugin.logging.Log;
import org.sonatype.plexus.build.incremental.BuildContext;
import ro.isdc.wro.manager.WroManager;
import ro.isdc.wro.manager.factory.WroManagerFactory;
import ro.isdc.wro.model.group.processor.InjectorBuilder;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.model.resource.processor.decorator.ExceptionHandlingProcessorDecorator;
import ro.isdc.wro.model.resource.processor.impl.css.AbstractCssImportPreProcessor;
import ro.isdc.wro.model.resource.processor.impl.css.CssImportPreProcessor;
import ro.isdc.wro.model.resource.support.hash.HashStrategy;
import ro.isdc.wro.util.Function;
import com.google.common.annotations.VisibleForTesting;
/**
* Encapsulates the details about resource change detection and persist the change information in build context.
*
* @author Alex Objelean
* @created 2 Oct 2013
* @since 1.7.2
*/
public class ResourceChangeHandler {
private enum ChangeStatus {
CHANGED, NOT_CHANGED
}
private WroManagerFactory managerFactory;
private Log log;
/**
* Responsible for build storage persistence. Uses configured {@link BuildContext} as a primary storage object.
*/
private BuildContextHolder buildContextHolder;
private BuildContext buildContext;
private File buildDirectory;
private boolean incrementalBuildEnabled;
/**
* Contains the set of already remembered resources. Used to avoid duplicate hash computation.
*/
private final Set<String> rememberedSet = new HashSet<String>();
/**
* Factory method which requires all mandatory fields.
*/
public static ResourceChangeHandler create(final WroManagerFactory managerFactory, final Log log) {
notNull(managerFactory, "WroManagerFactory was not set");
notNull(log, "Log was not set");
return new ResourceChangeHandler().setManagerFactory(managerFactory).setLog(log);
}
private ResourceChangeHandler() {
}
public boolean isResourceChanged(final Resource resource) {
notNull(resource, "Invalid resource provided");
final WroManager manager = getManagerFactory().create();
final HashStrategy hashStrategy = manager.getHashStrategy();
final UriLocatorFactory locatorFactory = manager.getUriLocatorFactory();
// using AtomicBoolean because we need to mutate this variable inside an anonymous class.
final AtomicBoolean changeDetected = new AtomicBoolean(false);
try {
final String fingerprint = hashStrategy.getHash(locatorFactory.locate(resource.getUri()));
final String previousFingerprint = getBuildContextHolder().getValue(resource.getUri());
final boolean newValue = fingerprint != null && !fingerprint.equals(previousFingerprint);
changeDetected.set(newValue);
if (!changeDetected.get() && resource.getType() == ResourceType.CSS) {
final Reader reader = new InputStreamReader(locatorFactory.locate(resource.getUri()));
getLog().debug("Check @import directive from " + resource);
// detect changes in imported resources.
detectChangeForCssImports(resource, reader, changeDetected);
}
return changeDetected.get();
} catch (final IOException e) {
getLog().error("failed to check for delta resource: " + resource, e);
}
return false;
}
private void detectChangeForCssImports(final Resource resource, final Reader reader,
final AtomicBoolean changeDetected)
throws IOException {
forEachCssImportApply(new Function<String, ChangeStatus>() {
public ChangeStatus apply(final String importedUri) throws Exception {
final boolean isImportChanged = isResourceChanged(Resource.create(importedUri, ResourceType.CSS));
getLog().debug("\tisImportChanged: " + isImportChanged);
if (isImportChanged) {
changeDetected.set(true);
return ChangeStatus.CHANGED;
}
return ChangeStatus.NOT_CHANGED;
}
}, resource, reader);
}
/**
* Will persist the information regarding the provided resource in some internal store. This information will be used
* later to check if the resource is changed.
*
* @param resource
* {@link Resource} to touch.
*/
public void remember(final Resource resource) {
final WroManager manager = getManagerFactory().create();
final HashStrategy hashStrategy = manager.getHashStrategy();
final UriLocatorFactory locatorFactory = manager.getUriLocatorFactory();
if (rememberedSet.contains(resource.getUri())) {
// only calculate fingerprints and check imports if not already done
getLog().debug("Resource with uri '" + resource.getUri() + "' has already been updated in this run.");
} else {
try {
final String fingerprint = hashStrategy.getHash(locatorFactory.locate(resource.getUri()));
getBuildContextHolder().setValue(resource.getUri(), fingerprint);
rememberedSet.add(resource.getUri());
getLog().debug("Persist fingerprint for resource '" + resource.getUri() + "' : " + fingerprint);
if (resource.getType() == ResourceType.CSS) {
final Reader reader = new InputStreamReader(locatorFactory.locate(resource.getUri()));
getLog().debug("Check @import directive from " + resource);
// persist fingerprints in imported resources.
persistFingerprintsForCssImports(resource, reader);
}
} catch (final IOException e) {
getLog().debug("could not check fingerprint of resource: " + resource);
}
}
}
private void persistFingerprintsForCssImports(final Resource resource, final Reader reader)
throws IOException {
forEachCssImportApply(new Function<String, ChangeStatus>() {
public ChangeStatus apply(final String importedUri) throws Exception {
remember(Resource.create(importedUri, ResourceType.CSS));
return ChangeStatus.NOT_CHANGED;
}
}, resource, reader);
}
/**
* Invokes the provided function for each detected css import.
*
* @param func
* a function (closure) invoked for each found import. It will be provided as argument the uri of imported
* css.
*/
private void forEachCssImportApply(final Function<String, ChangeStatus> func, final Resource resource, final Reader reader)
throws IOException {
final ResourcePreProcessor processor = createCssImportProcessor(func);
InjectorBuilder.create(getManagerFactory()).build().inject(processor);
processor.process(resource, reader, new StringWriter());
}
private ResourcePreProcessor createCssImportProcessor(final Function<String, ChangeStatus> func) {
final ResourcePreProcessor cssImportProcessor = new AbstractCssImportPreProcessor() {
@Override
protected void onImportDetected(final String importedUri) {
getLog().debug("Found @import " + importedUri);
try {
final ChangeStatus status = func.apply(importedUri);
getLog().debug("ChangeStatus for " + importedUri + ": " + status);
if (ChangeStatus.NOT_CHANGED.equals(status)) {
remember(Resource.create(importedUri, ResourceType.CSS));
}
} catch (final Exception e) {
getLog().error("Cannot apply a function on @import resource: " + importedUri + ". Ignoring it.", e);
}
remember(Resource.create(importedUri, ResourceType.CSS));
}
@Override
protected String doTransform(final String cssContent, final List<Resource> foundImports)
throws IOException {
// no need to build the content, since we are interested in finding imported resources only
return "";
}
@Override
public String toString() {
return CssImportPreProcessor.class.getSimpleName();
}
};
final ResourcePreProcessor processor = new ExceptionHandlingProcessorDecorator(cssImportProcessor) {
@Override
protected boolean isIgnoreFailingProcessor() {
return true;
}
};
return processor;
}
private BuildContextHolder getBuildContextHolder() {
if (buildContextHolder == null) {
buildContextHolder = new BuildContextHolder(buildContext, buildDirectory);
buildContextHolder.setIncrementalBuildEnabled(incrementalBuildEnabled);
}
return buildContextHolder;
}
@VisibleForTesting
void setBuildContextHolder(final BuildContextHolder buildContextHolder) {
this.buildContextHolder = buildContextHolder;
}
private WroManagerFactory getManagerFactory() {
return managerFactory;
}
public Log getLog() {
return log;
}
public ResourceChangeHandler setManagerFactory(final WroManagerFactory wroManagerFactory) {
this.managerFactory = wroManagerFactory;
return this;
}
public ResourceChangeHandler setLog(final Log log) {
this.log = log;
return this;
}
public ResourceChangeHandler setBuildContext(final BuildContext buildContext) {
this.buildContext = buildContext;
return this;
}
public ResourceChangeHandler setBuildDirectory(final File buildDirectory) {
this.buildDirectory = buildDirectory;
return this;
}
public ResourceChangeHandler setIncrementalBuildEnabled(final boolean incrementalBuildEnabled) {
this.incrementalBuildEnabled = incrementalBuildEnabled;
return this;
}
public boolean isIncrementalBuild() {
return getBuildContextHolder().isIncrementalBuild();
}
/**
* Destroys all information about resource tracked for changes.
*/
public void destroy() {
getBuildContextHolder().destroy();
rememberedSet.clear();
}
/**
* After invoking this method on a resource, the next invocation of {@link #isResourceChanged(Resource)} will return
* true.
*
* @param resourceUri
* uri of the resource to clear from persisted storage.
*/
public void forget(final Resource resource) {
if (resource != null) {
getBuildContextHolder().setValue(resource.getUri(), null);
rememberedSet.remove(resource.getUri());
}
}
/**
* Persist the values stored in BuildContext(Holder)
*/
public void persist() {
getBuildContextHolder().persist();
}
}