/* * 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.scanners.file; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.ignite.internal.util.lang.GridTuple; 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.apache.ignite.spi.deployment.uri.scanners.GridDeploymentFileHandler; import org.apache.ignite.spi.deployment.uri.scanners.GridDeploymentFolderScannerHelper; import org.apache.ignite.spi.deployment.uri.scanners.UriDeploymentScanner; import org.apache.ignite.spi.deployment.uri.scanners.UriDeploymentScannerContext; /** * URI deployment file scanner. */ public class UriDeploymentFileScanner implements UriDeploymentScanner { /** Default scan frequency. */ public static final int DFLT_SCAN_FREQ = 5000; /** Per-URI contexts. */ private final ConcurrentHashMap<URI, URIContext> uriCtxs = new ConcurrentHashMap<>(); /** {@inheritDoc} */ @Override public boolean acceptsURI(URI uri) { String proto = uri.getScheme().toLowerCase(); return "file".equals(proto); } /** {@inheritDoc} */ @Override public void scan(UriDeploymentScannerContext scanCtx) { URI uri = scanCtx.getUri(); URIContext uriCtx = uriCtxs.get(uri); if (uriCtx == null) { uriCtx = createUriContext(uri, scanCtx); URIContext oldUriCtx = uriCtxs.putIfAbsent(uri, uriCtx); if (oldUriCtx != null) uriCtx = oldUriCtx; } uriCtx.scan(scanCtx); } /** {@inheritDoc} */ @Override public long getDefaultScanFrequency() { return DFLT_SCAN_FREQ; } /** * Create context for the given URI. * * @param uri URI. * @param scanCtx Scanner context. * @return URI context. */ private URIContext createUriContext(URI uri, final UriDeploymentScannerContext scanCtx) { String scanDirPath = uri.getPath(); File scanDir = null; if (scanDirPath != null) scanDir = new File(scanDirPath); if (scanDir == null || !scanDir.isDirectory()) throw new IgniteSpiException("URI is either not provided or is not a directory: " + U.hidePassword(uri.toString())); FileFilter garFilter = new FileFilter() { /** {@inheritDoc} */ @Override public boolean accept(File pathname) { return scanCtx.getFilter().accept(null, pathname.getName()); } }; FileFilter garDirFilesFilter = new FileFilter() { /** {@inheritDoc} */ @Override public boolean accept(File pathname) { // Allow all files in GAR-directory. return pathname.isFile(); } }; return new URIContext(scanDir, garFilter, garDirFilesFilter); } /** * Converts given file name to the URI with "file" scheme. * * @param name File name to be converted. * @return File name with "file://" prefix. */ private static String getFileUri(String name) { assert name != null; name = name.replace("\\","/"); return "file://" + (name.charAt(0) == '/' ? "" : '/') + name; } /** * Context for the given URI. */ private static class URIContext { /** Scanning directory or file. */ private final File scanDir; /** GAR filter. */ private final FileFilter garFilter; /** GAR directory files filter. */ private final FileFilter garDirFilesFilter; /** Cache of found GAR-files or GAR-directories to check if any of it has been updated. */ private final Map<File, Long> tstampCache = new HashMap<>(); /** Cache of found files in GAR-folder to check if any of it has been updated. */ private final Map<File, Map<File, Long>> garDirFilesTstampCache = new HashMap<>(); /** * Constructor. * * @param scanDir Scan directory. * @param garFilter Gar filter. * @param garDirFilesFilter GAR directory files filter. */ private URIContext(File scanDir, FileFilter garFilter, FileFilter garDirFilesFilter) { this.scanDir = scanDir; this.garFilter = garFilter; this.garDirFilesFilter = garDirFilesFilter; } /** * Perform scan. * * @param scanCtx Scan context. */ private void scan(final UriDeploymentScannerContext scanCtx) { final Set<File> foundFiles = scanCtx.isFirstScan() ? new HashSet<File>() : U.<File>newHashSet(tstampCache.size()); GridDeploymentFileHandler hnd = new GridDeploymentFileHandler() { /** {@inheritDoc} */ @Override public void handle(File file) { foundFiles.add(file); handleFile(file, scanCtx); } }; // Scan directory for deploy units. GridDeploymentFolderScannerHelper.scanFolder(scanDir, garFilter, hnd); // Print warning if no GAR-units found first time. if (scanCtx.isFirstScan() && foundFiles.isEmpty()) U.warn(scanCtx.getLogger(), "No GAR-units found in: " + U.hidePassword(scanCtx.getUri().toString())); if (!scanCtx.isFirstScan()) { Collection<File> deletedFiles = new HashSet<>(tstampCache.keySet()); deletedFiles.removeAll(foundFiles); if (!deletedFiles.isEmpty()) { List<String> uris = new ArrayList<>(); for (File file : deletedFiles) { uris.add(getFileUri(file.getAbsolutePath())); } // Clear cache. tstampCache.keySet().removeAll(deletedFiles); garDirFilesTstampCache.keySet().removeAll(deletedFiles); scanCtx.getListener().onDeletedFiles(uris); } } } /** * Tests whether given directory or file was changed since last check and if so * copies all directory sub-folders and files or file itself to the deployment * directory and than notifies listener about new or updated files. * * @param file Scanning directory or file. * @param ctx Scanner context. */ private void handleFile(File file, UriDeploymentScannerContext ctx) { boolean changed; Long lastMod; if (file.isDirectory()) { GridTuple<Long> dirLastModified = F.t(file.lastModified()); changed = checkGarDirectoryChanged(file, dirLastModified); lastMod = dirLastModified.get(); } else { lastMod = tstampCache.get(file); changed = lastMod == null || lastMod != file.lastModified(); lastMod = file.lastModified(); } // If file is new or has been modified. if (changed) { tstampCache.put(file, lastMod); if (ctx.getLogger().isDebugEnabled()) ctx.getLogger().debug("Discovered deployment file or directory: " + file); String fileName = file.getName(); try { File cpFile = ctx.createTempFile(fileName, ctx.getDeployDirectory()); // Delete file when JVM stopped. cpFile.deleteOnExit(); if (file.isDirectory()) { cpFile = new File(cpFile.getParent(), "dir_" + cpFile.getName()); // Delete directory when JVM stopped. cpFile.deleteOnExit(); } // Copy file to deploy directory. U.copy(file, cpFile, true); String fileUri = getFileUri(file.getAbsolutePath()); assert lastMod != null; ctx.getListener().onNewOrUpdatedFile(cpFile, fileUri, lastMod); } catch (IOException e) { U.error(ctx.getLogger(), "Error saving file: " + fileName, e); } } } /** * Tests whether certain directory was changed since given modification date. * It scans all directory files one by one and compares their modification * dates with those ones that was collected before. * <p> * If at least one file was changed (has modification date after given one) * whole directory is considered as modified. * * @param dir Scanning directory. * @param lastModified Last calculated Directory modification date. * @return {@code true} if directory was changed since last check and * {@code false} otherwise. */ @SuppressWarnings("ConstantConditions") private boolean checkGarDirectoryChanged(File dir, final GridTuple<Long> lastModified) { final Map<File, Long> clssTstampCache; boolean firstScan = false; if (!garDirFilesTstampCache.containsKey(dir)) { firstScan = true; garDirFilesTstampCache.put(dir, clssTstampCache = new HashMap<>()); } else clssTstampCache = garDirFilesTstampCache.get(dir); assert clssTstampCache != null; final GridTuple<Boolean> changed = F.t(false); final Set<File> foundFiles = firstScan ? new HashSet<File>() : U.<File>newHashSet(clssTstampCache.size()); GridDeploymentFileHandler hnd = new GridDeploymentFileHandler() { @Override public void handle(File file) { foundFiles.add(file); Long fileLastModified = clssTstampCache.get(file); if (fileLastModified == null || fileLastModified != file.lastModified()) { clssTstampCache.put(file, fileLastModified = file.lastModified()); changed.set(true); } // Calculate last modified file in folder. if (fileLastModified > lastModified.get()) lastModified.set(fileLastModified); } }; // Scan GAR-directory for changes. GridDeploymentFolderScannerHelper.scanFolder(dir, garDirFilesFilter, hnd); // Clear cache for deleted files. if (!firstScan && clssTstampCache.keySet().retainAll(foundFiles)) changed.set(true); return changed.get(); } } }