rapture.plugin.install.PluginSandboxItem.java Source code

Java tutorial

Introduction

Here is the source code for rapture.plugin.install.PluginSandboxItem.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2011-2016 Incapture Technologies LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package rapture.plugin.install;

import static com.google.common.base.Preconditions.checkArgument;
import static rapture.common.Scheme.BLOB;
import static rapture.common.Scheme.DOCUMENT;
import static rapture.common.Scheme.EVENT;
import static rapture.common.Scheme.IDGEN;
import static rapture.common.Scheme.INDEX;
import static rapture.common.Scheme.JAR;
import static rapture.common.Scheme.JOB;
import static rapture.common.Scheme.SCRIPT;
import static rapture.common.Scheme.SERIES;
import static rapture.common.Scheme.TABLE;
import static rapture.common.Scheme.WORKFLOW;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.log4j.Logger;

import com.google.common.base.Objects;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Closeables;

import rapture.common.PluginTransportItem;
import rapture.common.RaptureURI;
import rapture.common.Scheme;
import rapture.common.api.ScriptingApi;
import rapture.common.exception.RaptureException;
import rapture.common.exception.RaptureExceptionFactory;
import rapture.util.MimeTypeResolver;

/**
 * This class maintains a two-way binding between a content file in the expanded directory for a PluginSandbox and the corresponding object on the rapture
 * server. URIs that can not encode to a content file will be rejected in the constructor.
 * 
 * @author mel
 */
public class PluginSandboxItem {

    private static final Logger log = Logger.getLogger(PluginSandboxItem.class);

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "PluginSandboxItem [uri=" + uri + ", variant=" + variant + ", file=" + file + ", fullFilePath="
                + fullFilePath + ", fileHash=" + fileHash + ", remoteHash=" + remoteHash + ", content="
                + Arrays.toString(getContent()) + ", fileCurrent=" + fileCurrent + ", remoteCurrent="
                + remoteCurrent + "]";
    }

    public static final String CONTENTDIR = "content/";
    public static final String REPODIR = "repo/";
    public static final String AUTHORITY = "/authority";
    private final RaptureURI uri;
    private final String variant;
    private File file; // the local file backing this feature
    private String fullFilePath;
    private String fileHash = null; // this is the hash of the content, not the
    // SandboxItem itself
    private String remoteHash = null;
    private byte[] content = null;
    private boolean fileCurrent = false;
    private boolean remoteCurrent = false;

    public PluginSandboxItem(RaptureURI uri, String variant) {
        checkArgument(isContent(uri));
        this.variant = variant;
        this.uri = uri;
        file = null;
    }

    public PluginSandboxItem(RaptureURI uri, File rootDir, String variant) {
        this.uri = uri;
        this.variant = variant;
        file = calculateFile(uri, rootDir, variant);
    }

    public PluginSandboxItem(RaptureURI uri, File rootDir, String variant, String remoteHash) {
        this(uri, rootDir, variant);
        this.remoteHash = remoteHash;
        remoteCurrent = true;
        try {
            fileCurrent = diffWithFile(remoteHash, false);
        } catch (Exception ex) {
            // ignore -- we only care bout the old file if it matches the hash
            // and can be read ok
        }
    }

    /**
     * For use when the source is a zip file.
     */
    public PluginSandboxItem(RaptureURI uri, String variant, String hash, byte[] content) {
        this.uri = uri;
        this.setContent(content);
        this.fileHash = hash;
        this.variant = variant;
    }

    public void deflate() {
        setContent(null);
    }

    public File getFile() {
        return file;
    }

    public boolean isFileCurrent() {
        return fileCurrent;
    }

    public boolean isRemoteCurrent() {
        return remoteCurrent;
    }

    public void updateFilePath(File rootDir) {
        File f = calculateFile(uri, rootDir, variant);
        if (!Objects.equal(f, file)) {
            file = f;
            fileCurrent = false;
        }
    }

    public static File calculateFile(RaptureURI uri, File rootDir, String variant) {
        if (isContent(uri)) {
            return new File(rootDir, calculatePath(uri, variant));
        }
        return null;
    }

    public static String calculatePath(RaptureURI uri, String variant) {
        // allow users to specify uris without docpaths for objects that dont require it e.g. user://rapture
        switch (uri.getScheme()) {
        case USER:
        case ENTITLEMENTGROUP:
            if (!uri.hasDocPath()) {
                return topDir(variant) + scrub(uri.getAuthority()) + getExtFromUri(uri);
            }
            break;
        default:
            break;
        }
        return topDir(variant) + scrub(uri.getShortPath()) + getExtFromUri(uri);
    }

    private static String getExtFromUri(RaptureURI uri) {
        if (!uri.hasAttribute()) {
            return ext2scheme.inverse().get(uri.getScheme());
        }
        switch (uri.getScheme()) {
        case BLOB:
            return ".bits";
        case SCRIPT:
            return "jjs".equals(uri.getAttribute()) ? ".jjs" : ".rfx";
        default:
            return "";
        }
    }

    private static String topDir(String variant) {
        return (variant == null) ? CONTENTDIR : variant + "/";
    }

    public static Pair<RaptureURI, String> calculateURI(File leafFile, File rootDir) {
        String rootPath = rootDir.getPath();
        String leafPath = unscrub(leafFile.getPath());
        checkArgument(leafPath.startsWith(rootPath), "File is not a child of the rootDir: " + leafPath);
        checkArgument(leafPath.charAt(rootPath.length()) == '/',
                "File is not a child of the rootDir (missing slash?): " + leafPath);
        String leafTail = leafPath.substring(rootPath.length() + 1);
        return calculateURI(leafTail);
    }

    public static Pair<RaptureURI, String> calculateURI(String leafTail) {
        String variant = (leafTail.startsWith(CONTENTDIR)) ? null : extractVariant(leafTail);
        Triple<String, String, Scheme> trip = extractScheme(leafTail.substring(variantLength(variant)));
        checkArgument(trip != null, "extraneuous file");
        RaptureURI uri = null;
        switch (trip.getRight()) {
        case USER:
        case ENTITLEMENTGROUP:
            if (trip.getLeft().indexOf("/") == -1) {
                uri = new RaptureURI(trip.getLeft(), trip.getRight());
            }
            break;
        default:
            break;
        }
        if (uri == null) {
            uri = RaptureURI.createFromFullPathWithAttribute(trip.getLeft(), trip.getMiddle(), trip.getRight());
        }
        return new ImmutablePair<RaptureURI, String>(uri, variant);
    }

    public static final Pair<RaptureURI, String> calculateURI(ZipEntry entry) {
        String leafTail = entry.getName();
        return calculateURI(leafTail);
    }

    private static String extractVariant(String path) {
        int cut = path.indexOf('/');
        if (cut < 0)
            return null;
        return path.substring(0, cut);
    }

    private static int variantLength(String variant) {
        return (variant == null) ? CONTENTDIR.length() : (variant.length() + 1);
    }

    /**
     * Split the path of a filename into the extensionless path (for URI construction) and the URI scheme (based on the file extension)
     * 
     * @param path
     *            The relative path within the contents directory
     * @return a triple of the path (left), the attribute(middle), and the URI scheme (right)
     */
    public static Triple<String, String, Scheme> extractScheme(String path) {
        int chop = path.lastIndexOf('.');
        if (chop < 0)
            return null;
        String schemeName = path.substring(chop);

        String attr = null;
        Scheme scheme = ext2scheme.get(schemeName);
        if (scheme == null) {
            scheme = raw2scheme.get(schemeName);
            if (scheme == null) {
                scheme = BLOB; // Oooerr - unmatched extensions will be blobs (THIS IS GOOD, BELIEVE ME)
            } else {
                path = path.substring(0, chop);
            }
            switch (scheme) {
            case BLOB:
                attr = getResolver().getMimeTypeFromPath(path);
                break;
            case SCRIPT:
                attr = schemeName.equals(".jjs") ? "jjs" : "raw";
                break;
            default:
                throw new Error("Severe: Unhandled scheme in raw2scheme map"); // can only happen if a bad checkin is made
            }
        } else {
            path = path.substring(0, chop);
        }
        return ImmutableTriple.of(path, attr, scheme);
    }

    public void cacheFileContent() throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        setContent(PluginContentReader.readFromFile(file, md));
        fileHash = Hex.encodeHexString(md.digest());
        fileCurrent = true;
    }

    private static MimeTypeResolver resolver = null;

    private static MimeTypeResolver getResolver() {
        if (resolver == null)
            resolver = new MimeTypeResolver();
        return resolver;
    }

    public byte[] getFileContent(boolean forceRefresh) throws NoSuchAlgorithmException, IOException {
        if (!fileCurrent || forceRefresh) {
            cacheFileContent();
        }
        return getContent();
    }

    public void clearCache() {
        setContent(null);
    }

    /**
     * @return true if this content is a change from the existing content
     */
    public boolean setContentFromRemote(byte[] content, String hash) {
        this.setContent(content);
        remoteCurrent = true;
        remoteHash = hash;
        if (fileHash != null && !fileHash.equals(remoteHash)) {
            fileHash = null;
            fileCurrent = false;
            return true;
        }
        return fileHash != null;
    }

    /**
     * Hash-based comparisson with optional caching side-effects
     * 
     * @param hash
     *            the external hexified MD5 hash to compare against
     * @param cacheIfDifferent
     *            if set, the file contents will be cached
     * @return true if different, false if same
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public boolean diffWithFile(String hash, boolean cacheIfDifferent)
            throws IOException, NoSuchAlgorithmException {
        boolean cached = false;
        byte[] oldContent = getContent();
        if (fileHash == null) {
            try {
                cacheFileContent();
            } catch (FileNotFoundException ex) {
                setContent(cacheIfDifferent ? null : oldContent);
                return true;
            }
            cached = true;
        }

        boolean different = !Objects.equal(hash, fileHash);

        if (!cached && cacheIfDifferent && different) {
            cacheFileContent();
        } else if (!cacheIfDifferent) {
            setContent(oldContent);
        }
        return different;
    }

    public void storeFile() throws IOException {
        if (getContent() == null)
            return;
        PrintWriter writer = null;
        boolean early = true;
        try {
            file.getParentFile().mkdirs();
            writer = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            writer.print(getContent());
            early = false;
            fileCurrent = true;
        } finally {
            Closeables.close(writer, early);
        }
    }

    public static final BiMap<String, Scheme> ext2scheme = ImmutableBiMap.<String, Scheme>builder()
            .put(".script", SCRIPT).put(".series", SERIES).put(".blob", BLOB).put(".jar", JAR)
            .put(".rdoc", DOCUMENT).put(".idgen", IDGEN).put(".revent", EVENT).put(".table", TABLE)
            .put(".index", INDEX).put(".job", JOB).put(".workflow", WORKFLOW).put(".lock", Scheme.LOCK)
            .put(".snippet", Scheme.SNIPPET).put(".structured", Scheme.STRUCTURED).put(".field", Scheme.FIELD)
            .put(".structure", Scheme.STRUCTURE).put(".ftransform", Scheme.FIELDTRANSFORM)
            .put(".entity", Scheme.ENTITY).put(".widget", Scheme.WIDGET).put(".user", Scheme.USER)
            .put(".group", Scheme.ENTITLEMENTGROUP).put(".entitlement", Scheme.ENTITLEMENT).build();

    // do not edit this without making matching edits to extractScheme and getExtFromURI
    // .jjs is extension for server side javascript
    public static final Map<String, Scheme> raw2scheme = ImmutableMap.of(".rfx", SCRIPT, ".jjs", SCRIPT, ".bits",
            BLOB);

    private static boolean isContent(RaptureURI uri) {
        return ext2scheme.containsValue(uri.getScheme());
    }

    /**
     * Download the encoded contents from the specified client
     * 
     * @return true if the contents were changed as a result of this call
     */
    public boolean download(ScriptingApi client, boolean force) {
        if (force || needExtract()) {
            PluginTransportItem item = null;
            try {
                item = client.getPlugin().getPluginItem(uri.toString());
            } catch (RaptureException ex) {
                return false;
            }
            if (item == null)
                return false;
            return setContentFromRemote(item.getContent(), item.getHash());
        }
        return false;
    }

    protected boolean needExtract() {
        return !remoteCurrent || getContent() == null || getHash() == null
                || (fileHash != null && !fileHash.equals(remoteHash));
    }

    public void delete() {
        if (file.exists()) {
            file.delete();
        }
    }

    public RaptureURI getURI() {
        return uri;
    }

    public void writeZipEntry(ZipOutputStream out, ScriptingApi client, boolean build) throws IOException {
        String filePath = calculatePath(uri, variant);
        ZipEntry entry = new ZipEntry(filePath);
        out.putNextEntry(entry);
        if (build && (file != null)) {
            setContent(Files.readAllBytes(Paths.get(file.getAbsolutePath())));
        } else {
            download(client, false);
        }
        if ((getContent() != null) && (getContent().length > 0)) {
            out.write(getContent());
        }
    }

    public String getHash() {

        if (fileHash != null) {
            return fileHash;
        }
        if (remoteHash != null) {
            return remoteHash;
        }
        return null;
    }

    /**
     * make transport item from local cache if present or local file if not
     * 
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public PluginTransportItem makeTransportItem() throws NoSuchAlgorithmException, IOException {
        if (getContent() == null) {
            cacheFileContent();
            if (getContent() == null) {
                throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST, "Content not present");
            }
        }
        PluginTransportItem result = new PluginTransportItem();
        result.setContent(getContent());
        result.setUri(uri.toString());
        result.setHash(getHash());
        return result;
    }

    public static String scrub(String in) {
        return in.replace("\\", "_BACKSLASH_");
    }

    public static String unscrub(String in) {
        return in.replace("_BACKSLASH_", "\\");
    }

    public String getFullFilePath() {
        return fullFilePath;
    }

    public void setFullFilePath(String fullFilePath) {
        this.fullFilePath = fullFilePath;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }
}