org.nebulaframework.core.job.archive.GridArchive.java Source code

Java tutorial

Introduction

Here is the source code for org.nebulaframework.core.job.archive.GridArchive.java

Source

/*
 * Copyright (C) 2008 Yohan Liyanage. 
 * 
 * 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.nebulaframework.core.job.archive;

import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nebulaframework.core.job.GridJob;
import org.nebulaframework.deployment.classloading.GridArchiveClassLoader;
import org.nebulaframework.util.hashing.SHA1Generator;
import org.nebulaframework.util.io.IOSupport;
import org.springframework.util.Assert;

/**
 * Represents an archived {@code GridJob}. An archived {@code GridJob} is a
 * special format of {@code JAR}, which is referred to as
 * {@code Nebula Archive}, identified by the extension {@code .nar}.
 * <p>
 * Nebula Archives allow required libraries ({@code .jar}) to be included
 * within the archive itself, unlike the standard {@code JAR} file format.
 * <p>
 * The structure of Nebula Archive is as follows: <code>
 *  <pre>
 *   META-INF/
 *           |
 *           - Manifest.mf
 *           - ...
 *   NEBULA-INF/
 *             |
 *             - lib /
 *                   |
 *                   - library1.jar
 *                   - ...
 *             - ...
 *   yourpackage /
 *               |
 *               - ...
 *   your.class
 *   ...
 * </pre>
 *  </code> The libraries are to be packaged in {@code NEBULA-INF/lib} directory.
 * A special class loader is used by Nebula Framework to load required classes
 * from the libraries included in a Nebula Archive. Refer to
 * {@link GridArchiveClassLoader} for additional information regarding class
 * loading.
 * <p>
 * {@code GridArchive} keeps the {@code byte[]} of a {@code .nar} file, and SHA1
 * Hash for the {@code byte[]}, for verification purposes. The hash for the
 * {@code byte[]} is generated at the time of creation of the
 * {@code GridArchive} instance for a {@code .nar} file. At each remote node,
 * this hash will be compared with a SHA1-Hash generated at the time, to ensure
 * that the {@code GridArchive} contains valid data.
 * <p>
 * To instantiate a {@code GridArchive}, use the following factory methods
 * <ul>
 * <li>{@link #fromFile(File)}</li>
 * </ul>
 * <p>
 * This class implements {@link Externalizable} interface, instead of
 * {@link Serializable} to improve performance in communications, by reducing
 * the data transfer amount and serialization time [Grosso, W. 2001. "Java RMI",
 * Section 10.7.1].
 * 
 * @author Yohan Liyanage
 * @version 1.0
 * 
 * @see GridJob
 * @see GridArchiveClassLoader
 */
public class GridArchive implements Serializable {

    private static final long serialVersionUID = -5657326124238562531L;

    private static Log log = LogFactory.getLog(GridArchive.class);

    /**
     * Default directory name for {@code NEBULA-INF} inside a Nebula Archive
     * file.
     */
    public static final String NEBULA_INF = "NEBULA-INF";

    /**
     * Default path for JAR Libraries inside a Nebula Archive file.
     */
    public static final String LIBRARY_PATH = NEBULA_INF + "/lib";

    private String[] jobClassNames; // Class Names of GridJobs in .nar
    private byte[] bytes; // bytes of .nar file
    private String hash; // SHA1 Hash for bytes

    /**
     * Constructs a {@code GridArchive} with given bytes of {@code .nar} file,
     * and the names of {@code GridJob} classes.
     * <p>
     * SHA-1 Hash for the given {@code byte[]} will be calculated during the
     * instantiation process.
     * <p>
     * Note that the constructor is of <b>{@code protected}</b> scope. To
     * instantiate this type, use the factory method {@link #fromFile(File)}.
     * 
     * @param bytes
     *            {@code byte[]} of {@code .nar} file
     * @param jobClassNames
     *            {@code String[]} of fully qualified class names of
     *            {@code GridJob} classes inside the .nar file.
     * 
     * @see #fromFile(File)
     */
    protected GridArchive(byte[] bytes, String[] jobClassNames) {
        super();

        // Assertions
        Assert.notNull(bytes);
        Assert.notNull(jobClassNames);

        this.bytes = bytes;
        this.jobClassNames = jobClassNames;

        // Generate SHA1 Hash for bytes
        hash = SHA1Generator.generateAsString(bytes);
    }

    /**
     * Returns the bytes of the {@code .nar} file, represented by this
     * {@code GridArchive} instance.
     * <p>
     * This is a clone of internal byte[]. Changes to the return value will not
     * be reflected by this GridArchive.
     * 
     * @return bytes of {@code .nar} file
     */
    public byte[] getBytes() {
        // Return a clone to protect internal state
        return bytes.clone();
    }

    /**
     * Returns the SHA-1 Hash generated at the time of creation of this
     * {@code GridArchive}, for the bytes of {@code .nar} file.
     * 
     * @return SHA-1 Hash as {@code String}
     */
    public String getHash() {
        return hash;
    }

    /**
     * Returns an array of {@code String}s, which contains fully qualified
     * class names of {@code GridJob}s inside the {@code .nar} file.
     * <p>
     * Note that this method returns a clone of the intenal object, and any
     * changes to the return value will not be reflected in this GridArchive
     * instance.
     * 
     * @return Class names of {@code GridJob}s inside {@code .nar} file
     */
    public String[] getJobClassNames() {
        return jobClassNames.clone();
    }

    /**
     * <b>Factory Method</b> to create a {@code GridArchive} instance for the
     * given {@code File} instance of a {@code .nar} file.
     * 
     * @param file
     *            {@code File} instance of {@code .nar} file.
     * 
     * @return {@code GridArchive} instance for given {@code .nar} file.
     * 
     * @throws GridArchiveException
     *             if processing of {@code File} failed.
     */
    public static GridArchive fromFile(File file) throws GridArchiveException {
        try {
            // Assertions
            Assert.notNull(file);

            // Verify file integrity
            if (!verify(file)) {
                throw new SecurityException("Grid Archive Verification failed of " + file);
            }

            // Detect the GridJob Class names
            String[] jobClassNames = findJobClassNames(file);

            // Read byte[] from File
            byte[] bytes = IOSupport.readBytes(new FileInputStream(file));

            // Create and return GridArchive
            return new GridArchive(bytes, jobClassNames);

        } catch (Exception e) {
            throw new GridArchiveException("Cannot create Grid Archive", e);
        }
    }

    /**
     * Verifies the integrity of the given {@code File}, as a Nebula Archive.
     * 
     * @param file
     *            {@code File} to be verified.
     * @return if success, {@code true}, otherwise {@code false}.
     */
    protected static boolean verify(File file) {
        // FIXME Implement to verify the NAR
        return file.exists();
    }

    /**
     * Returns the {@code GridJob} classes with in the given {@code .nar} file.
     * Uses {@link GridArchiveClassLoader}.
     * 
     * @param file
     *            {@code File} instance for {@code .nar} file.
     * 
     * @return Fully qualified class names of {@code GridJob} classes in the
     *         file.
     * 
     * @throws IOException
     *             if occurred during File I/O operations
     * 
     * @see GridArchiveClassLoader
     */
    protected static String[] findJobClassNames(final File file) throws IOException {

        // Instantiate ClassLoader for given File
        ClassLoader classLoader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
            public GridArchiveClassLoader run() {
                return new GridArchiveClassLoader(file);
            }

        });

        // Find ClassNames of all classes inside the file (except in NEBULA-INF)
        // Content inside .jar files will not be processed
        String[] allClassNames = getAllClassNames(file);

        // Holds Class<?> instances loaded by ClassLoader, for all classes
        List<String> jobClassNames = new ArrayList<String>();

        for (String className : allClassNames) {
            try {
                // Load each Class and check if its a GridJob Class
                if (isGridJobClass(classLoader.loadClass(className))) {
                    jobClassNames.add(className);
                }
            } catch (ClassNotFoundException e) {
                // Log and continue with rest
                log.debug("[GridArchive] Unable to load class " + className);
            }
        }
        return jobClassNames.toArray(new String[] {});
    }

    /**
     * Detects all classes inside the given {@code .nar} file and returns an
     * array of fully qualified class name of each class, as {@code String}.
     * 
     * @param file
     *            {@code .nar File}
     * 
     * @return Fully qualified class names classes in {@code File}
     * 
     * @throws IOException
     *             if occurred during File I/O operations
     */
    protected static String[] getAllClassNames(File file) throws IOException {

        // Holds Class Names
        List<String> names = new ArrayList<String>();

        // Create ZipArchive for File
        ZipFile archive = new ZipFile(file);
        Enumeration<? extends ZipEntry> entries = archive.entries();

        // Read each entry in archive
        while (entries.hasMoreElements()) {

            ZipEntry entry = entries.nextElement();

            // Ignore Directories
            if (entry.isDirectory())
                continue;

            // Ignore content in NEBULA-INF
            if (entry.getName().startsWith(GridArchive.NEBULA_INF)) {
                continue;
            }

            // Add each file which is a valid class file to list
            if (isClass(entry.getName())) {
                names.add(toClassName(entry.getName()));
            }
        }
        return names.toArray(new String[] {});
    }

    /**
     * Detects whether a given {@code Class} implements
     * {@code GridJob interface}, using Reflection API.
     * 
     * @param cls
     *            {@code Class} to be checked
     * @return if {@code GridJob} class, {@code true}, otherwise {@code false}
     */
    protected static boolean isGridJobClass(Class<?> cls) {

        // Get all interfaces, and process each
        for (Class<?> iface : cls.getInterfaces()) {
            // If class implements GridJob interfaces
            if (isGridJobInterface(iface)) {
                log.debug("[GridArchive] Found GridJob Class " + cls.getName());
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if the given interface is a sub-interface of {@code GridJob}
     * marker interface.
     * 
     * @param intrface
     *            interface to check
     * @return if {@code GridJob}, {@code true}, otherwise {@code false}
     */
    private static boolean isGridJobInterface(Class<?> intrface) {
        for (Class<?> iface : intrface.getInterfaces()) {
            if (iface.getName().equals(GridJob.class.getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Converts the given file name to fully qualified class name. For example,
     * for '{@code org/nebulaframework/Grid.class}', this method returns '{@code org.nebulaframework.Grid}'.
     * 
     * @param fileName
     *            File name to be converted
     * @return Fully qualified Class Name
     */
    protected static String toClassName(String fileName) {
        String name = fileName.substring(0, fileName.length() - ".class".length());
        return name.replaceAll("\\/|\\\\", "."); // Replace all path
        // separators (Win/Linux)
    }

    /**
     * Returns {@code true} if the given file name (path) identifies a class
     * file. The identification is done by checking if the file name ends with '{@code .class}'.
     * 
     * @param fileName
     *            File Name to be checked
     * @return if class, {@code true}, otherwise {@code false}
     */
    protected static boolean isClass(String fileName) {
        return fileName.endsWith(".class");
    }

}