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.apache.commons.io.FileUtils;
7   import org.slf4j.Logger;
8   import org.slf4j.LoggerFactory;
9   
10  import java.io.File;
11  import java.io.IOException;
12  import java.util.ArrayList;
13  import java.util.Collection;
14  import java.util.Collections;
15  import java.util.HashMap;
16  import java.util.List;
17  import java.util.Map;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static com.google.common.base.Preconditions.checkNotNull;
21  
22  /**
23   * A Scanner which polls a file containing a list of file names to define its content.
24   *
25   * @since 3.0.24
26   */
27  public class RosterFileScanner implements Scanner {
28      private static final Logger log = LoggerFactory.getLogger(RosterFileScanner.class);
29  
30      final private File rosterFile;
31      private Map<String, DeploymentUnit> deploymentUnits;
32      private long lastModified;
33  
34      /**
35       * Create a roster file scanner which loads the given roster file to locate plugins.
36       * <p>
37       * The provided roster file format is determined by its file suffix. Currently supported suffixes are
38       * - {@link #getListSuffix()}: a list of file paths, one per line
39       * If the format is not recognized, an {@link IllegalArgumentException} is thrown. This can be checked before construction
40       * using {@link #isKnownRosterFileFormat(File)}.
41       *
42       * @param rosterFile the file to load the plugin roster from.
43       * @throws IllegalArgumentException if the suffix of rosterFile is not a supported file format
44       */
45      public RosterFileScanner(final File rosterFile) {
46          checkNotNull(rosterFile);
47          checkArgument(isKnownRosterFileFormat(rosterFile), "Roster file '%s' does not end with '%s'", rosterFile, getListSuffix());
48          this.rosterFile = rosterFile;
49          this.deploymentUnits = Collections.emptyMap();
50      }
51  
52      @Override
53      public Collection<DeploymentUnit> scan() {
54          try {
55              final List<DeploymentUnit> scanned = new ArrayList<>();
56              final long updatedLastModified = rosterFile.lastModified();
57              if ((updatedLastModified != 0) && (updatedLastModified != lastModified)) {
58                  // File exists and has changed, reload to implement the scan
59                  // These semantics are by analogy with DirectoryScanner, that we forget plugins that drop out of the list
60                  // FileUtils is not generic safe
61                  //noinspection unchecked
62                  final List<String> filePaths = (List<String>) FileUtils.readLines(rosterFile);
63                  final Map<String, DeploymentUnit> updatedDeploymentUnits = new HashMap<>(filePaths.size());
64                  for (final String filePath : filePaths) {
65                      final DeploymentUnit priorUnit = deploymentUnits.get(filePath);
66                      if (null == priorUnit) {
67                          // It's new, make a new DeploymentUnit and include it in the scan.
68                          final File file = new File(filePath);
69                          final File absoluteFile = file.isAbsolute() ? file : new File(rosterFile.getParentFile(), filePath);
70                          final DeploymentUnit deploymentUnit = new DeploymentUnit(absoluteFile);
71                          updatedDeploymentUnits.put(filePath, deploymentUnit);
72                          scanned.add(deploymentUnit);
73                      } else {
74                          // Keep the deployment unit from the previous run
75                          updatedDeploymentUnits.put(filePath, priorUnit);
76                      }
77                  }
78                  deploymentUnits = updatedDeploymentUnits;
79                  lastModified = updatedLastModified;
80                  return scanned;
81              }
82          } catch (final IOException eio) {
83              log.warn("Cannot read roster file '{}': {}", rosterFile.getAbsolutePath(), eio.getMessage());
84          }
85          // unreadable, unchanged or error - report nothing new
86          return Collections.emptyList();
87      }
88  
89      @Override
90      public Collection<DeploymentUnit> getDeploymentUnits() {
91          return Collections.unmodifiableCollection(deploymentUnits.values());
92      }
93  
94      @Override
95      public void reset() {
96          deploymentUnits = Collections.emptyMap();
97          lastModified = 0;
98      }
99  
100     @Override
101     public void remove(final DeploymentUnit deploymentUnit) throws PluginException {
102         // Remove is called during the plugin upgrade process, but since we don't own the files, we don't actually remove them.
103         // In fact, they're in our software tree and hence read-only anyway, so we can't remove them.
104         // We just ignore remove - while we could in principle track what we'd seen, that's a chunk of logic for not much gain.
105     }
106 
107     /**
108      * The suffix for a roster file containing a simple plugin list.
109      * <p>
110      * Roster files with this suffix have one literal path per line. Relative paths are resolved relative to the directory
111      * containing the list file. Entries are considered distinct or not based on the path used in the file - no canonicalization
112      * is forced.
113      *
114      * @return the suffix used for roster files containing just a list of plugin jar files.
115      */
116     public static String getListSuffix() {
117         return ".list";
118     }
119 
120     /**
121      * Check whether a proposed roster file is in a known format.
122      *
123      * @param rosterFile the roster file to check.
124      * @return true if the format of the roster file is known.
125      * @see RosterFileScanner
126      */
127     public static boolean isKnownRosterFileFormat(final File rosterFile) {
128         return rosterFile.getName().endsWith(getListSuffix());
129     }
130 }