org.jboss.errai.offline.linker.DefaultCacheManifestLinker.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.errai.offline.linker.DefaultCacheManifestLinker.java

Source

/*
 * Copyright (C) 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.jboss.errai.offline.linker;

import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.gwt.core.ext.Linker;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.AbstractLinker;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.LinkerOrder.Order;
import com.google.gwt.core.ext.linker.Shardable;
import com.google.gwt.core.ext.linker.Transferable;
import com.google.gwt.core.ext.linker.impl.SelectionInformation;
import com.google.gwt.dev.util.collect.HashSet;

/**
 * Linker that generates permutation specific HTML5 Cache Manifest files for
 * Errai applications.
 * 
 * This linker is based on <a
 * href="https://code.google.com/p/google-web-toolkit/source/browse/
 * trunk/dev/core/src/
 * com/google/gwt/core/linker/SimpleAppCacheLinker.java?r=10136
 * ">SimpleAppCacheLinker</a>.
 * 
 * <p>
 * To use:
 * <ol>
 * <li>Define the linker in your gwt.xml module descriptor:
 * 
 * <pre>
 *   {@code <define-linker name="offline" class="org.jboss.errai.offline.linker.DefaultCacheManifestLinker" />}
 *   {@code <add-linker name="offline" />}
 * </pre>
 * 
 * </li>
 * 
 * <li>Add {@code manifest="YOURMODULENAME/errai.appcache"} to the
 * {@code <html>} tag in your host page e.g.,
 * {@code <html manifest="mymodule/errai.appcache">} <br>
 * <br>
 * </li>
 * 
 * <li>Add a mime-mapping to your web.xml file (you can skip this step if you
 * deploy the errai-javaee-all.jar as part of your application):
 * 
 * <pre>
 * {@code <mime-mapping>
 *   <extension>manifest</extension>
 *   <mime-type>text/cache-manifest</mime-type>
 * </mime-mapping>
 * }
 * </pre>
 * 
 * </li>
 * 
 * <li>Make sure the errai-common.jar file is deployed as part of your
 * application. It contains a servlet that will provide the correct user-agent
 * specific manifest file in response to requests to
 * YOURMODULENAME/errai.appcache</li>
 * </ol>
 * 
 * <p>
 * To obtain manifests that contain other files in addition to those generated
 * by this linker, create a class that inherits from this one and overrides
 * {@code otherCachedFiles()}, and use it as a linker instead:
 * 
 * <pre>
 * {@code @Shardable}
 * {@code @LinkerOrder(Order.POST)}
 * public class MyCacheManifestLinker extends DefaultCacheManifestLinker {
 *   {@code @Override}
 *   protected String[] otherCachedFiles() {
 *     return new String[] {"/my-app/index.html","/my-app/css/application.css"};
 *   }
 * }
 * </pre>
 * 
 * @author Christian Sadilek <csadilek@redhat.com>
 */
@Shardable
@LinkerOrder(Order.POST)
public class DefaultCacheManifestLinker extends AbstractLinker {
    private static final String MANIFEST = "appcache.manifest";

    @Transferable
    private class PermutationCacheManifestArtifact extends Artifact<PermutationCacheManifestArtifact> {
        private static final long serialVersionUID = 1L;

        private final Set<String> cachedFiles = new HashSet<String>();
        private final String name;
        private final Map<String, String> props;

        public PermutationCacheManifestArtifact(Class<? extends Linker> linker, String name,
                Map<String, String> props) {
            super(linker);
            this.name = name;
            this.props = props;
        }

        @Override
        public int hashCode() {
            return cachedFiles.hashCode();
        }

        @Override
        protected int compareToComparableArtifact(PermutationCacheManifestArtifact o) {
            return name.compareTo(o.name);
        }

        @Override
        protected Class<PermutationCacheManifestArtifact> getComparableArtifactType() {
            return PermutationCacheManifestArtifact.class;
        }

        public void addCachedFile(String file) {
            cachedFiles.add(file);
        }

    }

    @Override
    public String getDescription() {
        return "to generate cache manifest files";
    }

    @Override
    public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, boolean onePermutation)
            throws UnableToCompleteException {

        final ArtifactSet toReturn = new ArtifactSet(artifacts);
        if (toReturn.find(SelectionInformation.class).isEmpty()) {
            logger.log(TreeLogger.INFO, "devmode: generating empty " + MANIFEST);
            toReturn.add(emitString(logger, "# Empty in DevMode\n", "dev." + MANIFEST));
        } else if (onePermutation) {
            // Create an artifact representing the cache manifest for the current
            // permutation
            toReturn.add(createPermutationCacheManifestArtifact(context, logger, artifacts));
        } else {
            // Group permutations per user agent
            final Multimap<String, PermutationCacheManifestArtifact> permutations = ArrayListMultimap.create();
            for (final PermutationCacheManifestArtifact pcma : artifacts
                    .find(PermutationCacheManifestArtifact.class)) {
                permutations.put(pcma.props.get("user.agent"), pcma);
            }

            for (final String userAgent : permutations.keySet()) {
                // Create a cache manifest file for every user agent
                toReturn.add(
                        emitUserAgentCacheManifestFile(userAgent, permutations.get(userAgent), artifacts, logger));

            }

            logger.log(TreeLogger.INFO,
                    "Make sure you have the following attribute added to your host page's <html> tag: <html manifest=\""
                            + context.getModuleFunctionName() + "/" + MANIFEST + "\">");
        }
        return toReturn;
    }

    /**
     * Override this method to include additional files in the manifest.
     */
    protected String[] otherCachedFiles() {
        return null;
    }

    /**
     * Creates the cache manifest artifact specific to the current permutation.
     * 
     * @param context
     *          the linker environment
     * @param logger
     *          the tree logger to record to
     * @param artifacts
     *          {@code null} in case no permutation specific artifacts exist
     */
    private Artifact<?> createPermutationCacheManifestArtifact(LinkerContext context, TreeLogger logger,
            ArtifactSet artifacts) {

        final SelectionInformation si = artifacts.find(SelectionInformation.class).first();
        final PermutationCacheManifestArtifact cacheArtifact = new PermutationCacheManifestArtifact(this.getClass(),
                si.getStrongName(), si.getPropMap());

        for (final Artifact<?> artifact : artifacts) {
            if (artifact instanceof EmittedArtifact) {
                final EmittedArtifact ea = (EmittedArtifact) artifact;
                final String pathName = ea.getPartialPath();
                if (shouldBeCached(pathName)) {
                    cacheArtifact.addCachedFile(pathName);
                }
            }
        }
        return cacheArtifact;
    }

    /**
     * Emits the cache manifest file for the user agent represented by the
     * provided artifacts.
     * 
     * @param userAgent
     *          the user agent name
     * @param artifacts
     *          the user agent specific cache manifest artifacts
     * @param globalArtifacts
     *          all artifacts known to this linker
     * @param logger
     *          the tree logger to record to
     * @return a synthetic artifact representing the cache manifest file for the
     *         user agent
     * 
     * @throws UnableToCompleteException
     */
    private Artifact<?> emitUserAgentCacheManifestFile(String userAgent,
            Collection<PermutationCacheManifestArtifact> artifacts, ArtifactSet globalArtifacts, TreeLogger logger)
            throws UnableToCompleteException {

        // Add static external resources
        final StringBuilder staticResoucesSb = new StringBuilder();
        final String[] cacheExtraFiles = getCacheExtraFiles();
        for (int i = 0; i < cacheExtraFiles.length; i++) {
            staticResoucesSb.append(cacheExtraFiles[i]);
            staticResoucesSb.append("\n");
        }

        // Add generated resources
        final Set<String> cacheableGeneratedResources = new HashSet<String>();

        // Add permutation independent resources
        if (globalArtifacts != null) {
            for (final Artifact<?> a : globalArtifacts) {
                if (a instanceof EmittedArtifact) {
                    final EmittedArtifact ea = (EmittedArtifact) a;
                    final String pathName = ea.getPartialPath();
                    if (shouldBeCached(pathName) && !isPermutationSpecific(globalArtifacts, pathName)) {
                        cacheableGeneratedResources.add(pathName);
                    }
                }
            }
        }

        // Add permutation specific resources
        if (artifacts != null) {
            for (final PermutationCacheManifestArtifact artifact : artifacts) {
                for (final String cachedFile : artifact.cachedFiles) {
                    cacheableGeneratedResources.add(cachedFile);
                }
            }
        }

        // Build manifest
        final StringBuilder sb = new StringBuilder();
        sb.append("CACHE MANIFEST\n");
        // we have to generate this unique id because the resources can change but
        // the hashed cache.html files can remain the same.
        sb.append("# Unique id #" + (new Date()).getTime() + "." + Math.random() + "\n");
        sb.append("\n");
        sb.append("CACHE:\n");
        sb.append("# Static app files\n");
        sb.append(staticResoucesSb.toString());
        sb.append("\n# Generated permutation specific app files");
        for (final String resource : cacheableGeneratedResources) {
            sb.append("\n");
            sb.append(resource);
        }
        sb.append("\n\n");
        sb.append("# All other resources require the user to be online.\n");
        sb.append("NETWORK:\n");
        sb.append("*\n");

        // Create the user agent specific manifest as a new artifact and return it
        return emitString(logger, sb.toString(), userAgent + "." + MANIFEST);
    }

    /**
     * Obtains the extra files to include in the manifest. Ensures the returned
     * array is not null.
     */
    private String[] getCacheExtraFiles() {
        final String[] cacheExtraFiles = otherCachedFiles();
        return cacheExtraFiles == null ? new String[0] : Arrays.copyOf(cacheExtraFiles, cacheExtraFiles.length);
    }

    private Set<String> permutationFiles;

    /**
     * Checks whether the provided file is specific to a permutation.
     * 
     * @param artifacts
     *          all artifacts passed to this linker
     * @param file
     *          the file to check
     * @return true if the file is specific to a permutation, otherwise false.
     * 
     */
    private boolean isPermutationSpecific(ArtifactSet artifacts, String file) {
        if (permutationFiles == null) {
            permutationFiles = new HashSet<String>();
            for (final PermutationCacheManifestArtifact pcma : artifacts
                    .find(PermutationCacheManifestArtifact.class)) {
                permutationFiles.addAll(pcma.cachedFiles);
            }
        }

        return permutationFiles.contains(file);
    }

    /**
     * Checks whether or not the provided file should be cached.
     * 
     * @param file
     *          the file to check
     * @return true if the file is cacheable and should therefore be listed in the
     *         cache manifest file, otherwise false.
     */
    private boolean shouldBeCached(String file) {
        return !(file.endsWith("symbolMap") || file.endsWith(".xml.gz") || file.endsWith("rpc.log")
                || file.endsWith("gwt.rpc") || file.endsWith("manifest.txt") || file.startsWith("rpcPolicyManifest")
                || file.startsWith("deferredjs") || file.startsWith("hosted") || file.startsWith("junit"));
    }
}