org.apache.ignite.spi.deployment.uri.GridUriDeploymentFileProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.ignite.spi.deployment.uri.GridUriDeploymentFileProcessor.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.ignite.spi.deployment.uri;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.compute.ComputeTask;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.spi.IgniteSpiException;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.spi.deployment.uri.UriDeploymentSpi.XML_DESCRIPTOR_PATH;

/**
 * Utility class.
 * <p>
 * Provides useful and common functions for URI deployment.
 */
final class GridUriDeploymentFileProcessor {
    /**
     * Enforces singleton.
     */
    private GridUriDeploymentFileProcessor() {
        // No-op.
    }

    /**
     * Method processes given GAR file and extracts all tasks from it which are
     * either mentioned in GAR descriptor or implements interface {@link org.apache.ignite.compute.ComputeTask}
     * if there is no descriptor in file.
     *
     * @param file GAR file with tasks.
     * @param uri GAR file deployment URI.
     * @param deployDir deployment directory with downloaded GAR files.
     * @param log Logger.
     * @throws org.apache.ignite.spi.IgniteSpiException Thrown if file could not be read.
     * @return List of tasks from given file.
     */
    @Nullable
    static GridUriDeploymentFileProcessorResult processFile(File file, String uri, File deployDir, IgniteLogger log)
            throws IgniteSpiException {
        File gar = file;

        if (!checkIntegrity(file, log)) {
            U.error(log, "Tasks in GAR not loaded in configuration (invalid file signature) [uri="
                    + U.hidePassword(uri) + ']');

            return null;
        }

        if (!file.isDirectory()) {
            gar = new File(deployDir, "dirzip_" + file.getName());

            gar.mkdirs();

            try {
                U.unzip(file, gar, log);
            } catch (IOException e) {
                throw new IgniteSpiException("IO error when unzipping GAR file: " + file.getAbsolutePath(), e);
            }
        }

        GridUriDeploymentFileProcessorResult res = null;

        if (gar.isDirectory()) {
            try {
                File xml = new File(gar, XML_DESCRIPTOR_PATH);

                if (!xml.exists() || xml.isDirectory()) {
                    U.warn(log,
                            "Processing deployment without descriptor file (it will cause full classpath scan) [path="
                                    + XML_DESCRIPTOR_PATH + ", gar=" + gar.getAbsolutePath() + ']');

                    res = processNoDescriptorFile(gar, uri, log);
                } else {
                    InputStream in = null;

                    try {
                        in = new BufferedInputStream(new FileInputStream(xml));

                        // Parse XML task definitions and add them to cache.
                        GridUriDeploymentSpringDocument doc = GridUriDeploymentSpringParser.parseTasksDocument(in,
                                log);

                        assert doc != null;

                        res = processWithDescriptorFile(doc, gar, uri, log);
                    } finally {
                        U.close(in, log);
                    }
                }
            } catch (IOException e) {
                throw new IgniteSpiException("IO error when parsing GAR directory: " + gar.getAbsolutePath(), e);
            }
        }

        if (res != null)
            res.setMd5(md5(gar, log));

        return res;
    }

    /**
     * Calculates md5 checksum for the given file o directory.
     * For directories tries to walk all nested files accumulating the result.
     *
     * @param file file to calculate sum or root directory for accumulating calculation.
     * @param log logger to log all failures.
     * @return string representation of the calculated checksum or {@code null} if calculation failed.
     */
    @Nullable
    public static String md5(@Nullable File file, @Nullable IgniteLogger log) {
        if (file != null)
            return file.isFile() ? fileMd5(file, log) : directoryMd5(file, log);

        return null;
    }

    /**
     * Calculates md5 checksum for the given file
     *
     * @param file file to calculate md5.
     * @param log logger to log all failures.
     * @return string representation of the calculated checksum or {@code null} if calculation failed.
     */
    @Nullable
    public static String fileMd5(@Nullable File file, @Nullable IgniteLogger log) {
        String md5 = null;

        if (file != null) {
            if (!file.isFile()) {
                U.warn(log, "Failed to find file for md5 calculation: " + file);

                return null;
            }

            InputStream in = null;

            try {
                in = new BufferedInputStream(new FileInputStream(file));

                md5 = DigestUtils.md5Hex(in);
            } catch (IOException e) {
                U.warn(log, "Failed to open input stream for md5 calculation: " + e.getMessage());
            } finally {
                U.closeQuiet(in);
            }
        }

        return md5;
    }

    /**
     * For directories tries to walk all nested files accumulating them into single md5 checksum.
     *
     * @param dir directory to calculate md5.
     * @param log logger to log all failures.
     * @return string representation of the calculated checksum or {@code null} if calculation failed.
     */
    @Nullable
    public static String directoryMd5(@Nullable File dir, @Nullable IgniteLogger log) {
        if (dir != null) {
            if (!dir.isDirectory()) {
                U.warn(log, "Failed to find directory for md5 calculation: " + dir);

                return null;
            }

            try {
                MessageDigest digest = MessageDigest.getInstance("MD5");

                return addDirectoryDigest(dir, digest, log) ? Hex.encodeHexString(digest.digest()) : null;
            } catch (NoSuchAlgorithmException e) {
                throw new IgniteException("MD5 digest algorithm not found.", e);
            }
        }

        return null;
    }

    /**
     * Repulsively adds all files in the given directory to the given Digest object.
     *
     * @param file directory to start calculation from.
     * @param digest digest object where all available files should be applied.
     * @param log logger to report errors.
     * @return {@code true} if digest was added successfully, {@code false} otherwise.
     */
    private static boolean addDirectoryDigest(File file, MessageDigest digest, @Nullable IgniteLogger log) {
        assert file.isDirectory();

        File[] files = file.listFiles();

        if (files == null)
            return true;

        for (File visited : files) {
            if (visited.isFile()) {
                if (!addFileDigest(visited, digest, log))
                    return false;
            } else if (visited.isDirectory()) {
                if (!addDirectoryDigest(visited, digest, log))
                    return false;
            }
        }

        return true;
    }

    /**
     * Adds given file to the given Digest object.
     *
     * @param file file for digest calculations.
     * @param digest digest object to add file.
     * @param log logger to report errors.
     * @return {@code true} if digest was added successfully, {@code false} otherwise.
     */
    private static boolean addFileDigest(File file, MessageDigest digest, @Nullable IgniteLogger log) {
        if (!file.isFile()) {
            U.error(log, "Failed to add file to directory digest (will not check MD5 hash): " + file);

            return false;
        }

        InputStream in = null;

        try {
            in = new BufferedInputStream(new FileInputStream(file));

            byte[] buf = new byte[1024];

            int read = in.read(buf, 0, 1024);

            while (read > -1) {
                digest.update(buf, 0, read);

                read = in.read(buf, 0, 1024);
            }
        } catch (IOException e) {
            U.error(log, "Failed to add file to directory digest (will not check MD5 hash): " + file, e);

            return false;
        } finally {
            U.closeQuiet(in);
        }

        return true;
    }

    /**
     * Cleanup class loaders resource.
     *
     * @param clsLdr Released class loader.
     * @param log Logger.
     */
    static void cleanupUnit(ClassLoader clsLdr, IgniteLogger log) {
        assert clsLdr != null;
        assert log != null;

        if (clsLdr instanceof URLClassLoader) {
            URLClassLoader clsLdr0 = (URLClassLoader) clsLdr;

            U.close(clsLdr0, log);

            try {
                URL url = clsLdr0.getURLs()[0];

                File dir = new File(url.toURI());

                U.delete(dir);

                if (dir.getName().startsWith("dirzip_")) {
                    File jarFile = new File(dir.getParentFile(), dir.getName().substring(7));

                    U.delete(jarFile);
                }
            } catch (Exception e) {
                U.error(log, "Failed to cleanup unit [clsLdr=" + clsLdr + ']', e);
            }
        }
    }

    /**
     * Processes given GAR file and returns back all tasks which are in
     * descriptor.
     *
     * @param doc GAR file descriptor.
     * @param file GAR file.
     * @param uri GAR file deployment URI.
     * @param log Logger.
     * @throws org.apache.ignite.spi.IgniteSpiException Thrown if it's impossible to open file.
     * @return List of tasks from descriptor.
     */
    @SuppressWarnings({ "ClassLoader2Instantiation" })
    private static GridUriDeploymentFileProcessorResult processWithDescriptorFile(
            GridUriDeploymentSpringDocument doc, File file, String uri, IgniteLogger log)
            throws IgniteSpiException {
        ClassLoader clsLdr = GridUriDeploymentClassLoaderFactory.create(U.gridClassLoader(), file, log);

        List<Class<? extends ComputeTask<?, ?>>> tasks = doc.getTasks(clsLdr);

        List<Class<? extends ComputeTask<?, ?>>> validTasks = null;

        if (!F.isEmpty(tasks)) {
            validTasks = new ArrayList<>();

            for (Class<? extends ComputeTask<?, ?>> task : tasks) {
                if (!isAllowedTaskClass(task)) {
                    U.warn(log,
                            "Failed to load task. Task should be public none-abstract class "
                                    + "(might be inner static one) that implements ComputeTask interface [taskCls="
                                    + task + ']');
                } else {
                    if (log.isDebugEnabled())
                        log.debug("Found grid deployment task: " + task.getName());

                    validTasks.add(task);
                }
            }
        }

        GridUriDeploymentFileProcessorResult res = new GridUriDeploymentFileProcessorResult();

        res.setFile(file);
        res.setClassLoader(clsLdr);

        if (!F.isEmpty(validTasks))
            res.setTaskClasses(validTasks);
        else if (log.isDebugEnabled())
            log.debug("No tasks loaded from file [file=" + file.getAbsolutePath() + ", uri=" + U.hidePassword(uri)
                    + ']');

        return res;
    }

    /**
     * Processes GAR files which have no descriptor. It scans every class and
     * checks if it is a valid task or not. All valid tasks are returned back.
     *
     * @param file GAR file or directory.
     * @param uri GAR file deployment URI.
     * @param log Logger.
     * @throws org.apache.ignite.spi.IgniteSpiException Thrown if file reading error happened.
     * @return List of tasks from given file.
     */
    private static GridUriDeploymentFileProcessorResult processNoDescriptorFile(File file, String uri,
            IgniteLogger log) throws IgniteSpiException {
        ClassLoader clsLdr = GridUriDeploymentClassLoaderFactory.create(U.gridClassLoader(), file, log);

        Set<Class<? extends ComputeTask<?, ?>>> clss = GridUriDeploymentDiscovery.getClasses(clsLdr, file);

        GridUriDeploymentFileProcessorResult res = new GridUriDeploymentFileProcessorResult();

        res.setFile(file);
        res.setClassLoader(clsLdr);

        if (clss != null) {
            List<Class<? extends ComputeTask<?, ?>>> validTasks = new ArrayList<>(clss.size());

            for (Class<? extends ComputeTask<?, ?>> cls : clss) {
                if (isAllowedTaskClass(cls)) {
                    if (log.isDebugEnabled())
                        log.debug("Found grid deployment task: " + cls.getName());

                    validTasks.add(cls);
                }
            }

            if (!validTasks.isEmpty())
                res.setTaskClasses(validTasks);
            else if (log.isDebugEnabled())
                log.debug("No tasks loaded from file [file=" + file.getAbsolutePath() + ", uri="
                        + U.hidePassword(uri) + ']');
        }

        return res;
    }

    /**
     * Check that class may be instantiated as {@link org.apache.ignite.compute.ComputeTask} and used
     * in deployment.
     *
     * Loaded task class must implement interface {@link org.apache.ignite.compute.ComputeTask}.
     * Only non-abstract, non-interfaces and public classes allowed.
     * Inner static classes also allowed for loading.
     *
     * @param cls Class to check
     * @return {@code true} if class allowed for deployment.
     */
    private static boolean isAllowedTaskClass(Class<?> cls) {
        if (!ComputeTask.class.isAssignableFrom(cls))
            return false;

        int modifiers = cls.getModifiers();

        return !Modifier.isAbstract(modifiers) && !Modifier.isInterface(modifiers)
                && (!cls.isMemberClass() || Modifier.isStatic(modifiers)) && Modifier.isPublic(modifiers);
    }

    /**
     * Make integrity check for GAR file.
     * Method returns {@code false} if GAR file has incorrect signature.
     *
     * @param file GAR file which should be verified.
     * @param log Logger.
     * @return {@code true} if given file is a directory of verification
     *      completed successfully otherwise returns {@code false}.
     */
    private static boolean checkIntegrity(File file, IgniteLogger log) {
        try {
            return file.isDirectory() || GridUriDeploymentJarVerifier.verify(file.getAbsolutePath(), false, log);
        } catch (IOException e) {
            U.error(log, "Error while making integrity file check.", e);
        }

        return false;
    }
}