View Javadoc
1   package com.atlassian.plugin.loaders;
2   
3   import com.atlassian.plugin.PluginException;
4   import com.atlassian.plugin.loaders.classloading.DeploymentUnit;
5   import com.atlassian.plugin.loaders.classloading.Scanner;
6   import org.slf4j.Logger;
7   import org.slf4j.LoggerFactory;
8   
9   import java.io.File;
10  import java.io.FileNotFoundException;
11  import java.io.IOException;
12  import java.nio.file.AccessDeniedException;
13  import java.nio.file.Files;
14  import java.nio.file.NoSuchFileException;
15  import java.util.ArrayList;
16  import java.util.Arrays;
17  import java.util.Collection;
18  import java.util.Collections;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.TreeMap;
22  
23  import static com.google.common.base.Preconditions.checkNotNull;
24  
25  /**
26   * Scans the filesystem for changed or added plugin files and stores a map of the currently known ones. Files beginning
27   * with "." are ignored.
28   *
29   * @since 2.1.0
30   */
31  public class DirectoryScanner implements Scanner {
32  
33      private static Logger log = LoggerFactory.getLogger(DirectoryScanner.class);
34  
35      /**
36       * Tracks the classloading
37       */
38      private final File pluginsDirectory;
39  
40      /**
41       * A Map of {@link String} absolute file paths to {@link DeploymentUnit}s.
42       */
43      private final Map<String, DeploymentUnit> scannedDeploymentUnits = new TreeMap<>();
44  
45      /**
46       * Constructor for scanner.
47       *
48       * @param pluginsDirectory the directory that the scanner should monitor for plugins
49       */
50      public DirectoryScanner(final File pluginsDirectory) {
51          this.pluginsDirectory = checkNotNull(pluginsDirectory);
52      }
53  
54      private DeploymentUnit createAndStoreDeploymentUnit(final File file) {
55          if (isScanned(file))
56              return null;
57  
58          final DeploymentUnit unit = new DeploymentUnit(file);
59          scannedDeploymentUnits.put(file.getAbsolutePath(), unit);
60  
61          return unit;
62      }
63  
64      /**
65       * Given a file, finds the deployment unit for it if one has already been scanned.
66       *
67       * @param file a jar file.
68       * @return the stored deploymentUnit matching the file or null if none exists.
69       */
70      public DeploymentUnit locateDeploymentUnit(final File file) {
71          return scannedDeploymentUnits.get(file.getAbsolutePath());
72      }
73  
74      /**
75       * Finds whether the given file has been scanned already.
76       */
77      private boolean isScanned(final File file) {
78          return locateDeploymentUnit(file) != null;
79      }
80  
81      /**
82       * Tells the Scanner to forget about a file it has loaded so that it will reload it
83       * next time it scans.
84       *
85       * @param file a file that may have already been scanned.
86       */
87      public void clear(final File file) {
88          scannedDeploymentUnits.remove(file.getAbsolutePath());
89      }
90  
91      /**
92       * Scans for all files and directories that have been added or modified since the
93       * last call to scan. This will ignore all files or directories starting with
94       * the '.' character.
95       *
96       * @return Collection of {@link DeploymentUnit}s that describe newly added files or directories.
97       */
98      public Collection<DeploymentUnit> scan() {
99          // Checks to see if we have deleted any of the deployment units.
100         final List<File> removedFiles = new ArrayList<>();
101         for (final DeploymentUnit unit : scannedDeploymentUnits.values()) {
102             if (!unit.getPath().exists() || !unit.getPath().canRead()) {
103                 removedFiles.add(unit.getPath());
104             }
105         }
106         clear(removedFiles);
107 
108         // Checks for new files that don't start in '.'
109         final Collection<DeploymentUnit> result = new ArrayList<>();
110         final File files[] = pluginsDirectory.listFiles((dir, name) -> !name.startsWith("."));
111 
112         if (files == null) {
113             log.error("listFiles returned null for directory " + pluginsDirectory.getAbsolutePath());
114             return result;
115         }
116 
117         Arrays.sort(files); // sorts by filename for deterministic load order
118         for (final File file : files) {
119             if (isScanned(file) && isModified(file)) {
120                 clear(file);
121                 final DeploymentUnit unit = createAndStoreDeploymentUnit(file);
122                 if (unit != null)
123                     result.add(unit);
124             } else if (!isScanned(file)) {
125                 final DeploymentUnit unit = createAndStoreDeploymentUnit(file);
126                 if (unit != null)
127                     result.add(unit);
128             }
129         }
130         return result;
131     }
132 
133     private boolean isModified(final File file) {
134         final DeploymentUnit unit = locateDeploymentUnit(file);
135         return file.lastModified() > unit.lastModified();
136     }
137 
138     private void clear(final List<File> toUndeploy) {
139         for (final File aToUndeploy : toUndeploy) {
140             clear(aToUndeploy);
141         }
142     }
143 
144     /**
145      * Retrieve all the {@link DeploymentUnit}s currently stored.
146      *
147      * @return the complete unmodifiable list of scanned {@link DeploymentUnit}s.
148      */
149     public Collection<DeploymentUnit> getDeploymentUnits() {
150         return Collections.unmodifiableCollection(scannedDeploymentUnits.values());
151     }
152 
153     /**
154      * Clears the list of scanned deployment units.
155      */
156     public void reset() {
157         scannedDeploymentUnits.clear();
158     }
159 
160     public void remove(final DeploymentUnit unit) throws PluginException {
161         final File file = unit.getPath();
162         try {
163             //Rather than using File.delete(), which returns a boolean without giving much information on
164             //what actually failed when false is returned, use Files.delete(Path) so that exceptions will
165             //be thrown, allowing them to be handled accordingly.
166             //Note that exceptions, like AccessDeniedException and NoSuchFileException, are documented as
167             //"optional specific exceptions" that the underlying FileSystemProvider _can_ throw; they're
168             //not mandated by Files.delete(Path). In practice, however, these exceptions tend to be quite
169             //reliable.
170             Files.delete(file.toPath());
171         } catch (final AccessDeniedException e) {
172             log.info("Plugin file <{}> exists but we do not have permission to remove it. Ignoring.",
173                     file.getAbsolutePath());
174         } catch (final FileNotFoundException | NoSuchFileException e) {
175             log.debug("Plugin file <{}> doesn't exist to delete. Ignoring.", file.getAbsolutePath());
176         } catch (final IOException e) {
177             throw new PluginException("Unable to delete plugin file: " + file.getAbsolutePath());
178         }
179 
180         clear(file);
181     }
182 }