View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.repository.legacy;
20  
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.IOException;
24  import java.io.RandomAccessFile;
25  import java.nio.channels.Channels;
26  import java.nio.channels.FileChannel;
27  import java.nio.channels.FileLock;
28  import java.util.Date;
29  import java.util.Properties;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.repository.ArtifactRepository;
33  import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
34  import org.apache.maven.artifact.repository.Authentication;
35  import org.apache.maven.artifact.repository.metadata.RepositoryMetadata;
36  import org.apache.maven.repository.Proxy;
37  import org.codehaus.plexus.component.annotations.Component;
38  import org.codehaus.plexus.logging.AbstractLogEnabled;
39  import org.codehaus.plexus.logging.Logger;
40  
41  /**
42   * DefaultUpdateCheckManager
43   */
44  @Component(role = UpdateCheckManager.class)
45  public class DefaultUpdateCheckManager extends AbstractLogEnabled implements UpdateCheckManager {
46  
47      private static final String ERROR_KEY_SUFFIX = ".error";
48  
49      public DefaultUpdateCheckManager() {}
50  
51      public DefaultUpdateCheckManager(Logger logger) {
52          enableLogging(logger);
53      }
54  
55      public static final String LAST_UPDATE_TAG = ".lastUpdated";
56  
57      private static final String TOUCHFILE_NAME = "resolver-status.properties";
58  
59      public boolean isUpdateRequired(Artifact artifact, ArtifactRepository repository) {
60          File file = artifact.getFile();
61  
62          ArtifactRepositoryPolicy policy = artifact.isSnapshot() ? repository.getSnapshots() : repository.getReleases();
63  
64          if (!policy.isEnabled()) {
65              if (getLogger().isDebugEnabled()) {
66                  getLogger()
67                          .debug("Skipping update check for " + artifact + " (" + file + ") from " + repository.getId()
68                                  + " (" + repository.getUrl() + ")");
69              }
70  
71              return false;
72          }
73  
74          if (getLogger().isDebugEnabled()) {
75              getLogger()
76                      .debug("Determining update check for " + artifact + " (" + file + ") from " + repository.getId()
77                              + " (" + repository.getUrl() + ")");
78          }
79  
80          if (file == null) {
81              // TODO throw something instead?
82              return true;
83          }
84  
85          Date lastCheckDate;
86  
87          if (file.exists()) {
88              lastCheckDate = new Date(file.lastModified());
89          } else {
90              File touchfile = getTouchfile(artifact);
91              lastCheckDate = readLastUpdated(touchfile, getRepositoryKey(repository));
92          }
93  
94          return (lastCheckDate == null) || policy.checkOutOfDate(lastCheckDate);
95      }
96  
97      public boolean isUpdateRequired(RepositoryMetadata metadata, ArtifactRepository repository, File file) {
98          // Here, we need to determine which policy to use. Release updateInterval will be used when
99          // the metadata refers to a release artifact or meta-version, and snapshot updateInterval will be used when
100         // it refers to a snapshot artifact or meta-version.
101         // NOTE: Release metadata includes version information about artifacts that have been released, to allow
102         // meta-versions like RELEASE and LATEST to resolve, and also to allow retrieval of the range of valid, released
103         // artifacts available.
104         ArtifactRepositoryPolicy policy = metadata.getPolicy(repository);
105 
106         if (!policy.isEnabled()) {
107             if (getLogger().isDebugEnabled()) {
108                 getLogger()
109                         .debug("Skipping update check for " + metadata.getKey() + " (" + file + ") from "
110                                 + repository.getId() + " (" + repository.getUrl() + ")");
111             }
112 
113             return false;
114         }
115 
116         if (getLogger().isDebugEnabled()) {
117             getLogger()
118                     .debug("Determining update check for " + metadata.getKey() + " (" + file + ") from "
119                             + repository.getId() + " (" + repository.getUrl() + ")");
120         }
121 
122         if (file == null) {
123             // TODO throw something instead?
124             return true;
125         }
126 
127         Date lastCheckDate = readLastUpdated(metadata, repository, file);
128 
129         return (lastCheckDate == null) || policy.checkOutOfDate(lastCheckDate);
130     }
131 
132     private Date readLastUpdated(RepositoryMetadata metadata, ArtifactRepository repository, File file) {
133         File touchfile = getTouchfile(metadata, file);
134 
135         String key = getMetadataKey(repository, file);
136 
137         return readLastUpdated(touchfile, key);
138     }
139 
140     public String getError(Artifact artifact, ArtifactRepository repository) {
141         File touchFile = getTouchfile(artifact);
142         return getError(touchFile, getRepositoryKey(repository));
143     }
144 
145     public void touch(Artifact artifact, ArtifactRepository repository, String error) {
146         File file = artifact.getFile();
147 
148         File touchfile = getTouchfile(artifact);
149 
150         if (file.exists()) {
151             touchfile.delete();
152         } else {
153             writeLastUpdated(touchfile, getRepositoryKey(repository), error);
154         }
155     }
156 
157     public void touch(RepositoryMetadata metadata, ArtifactRepository repository, File file) {
158         File touchfile = getTouchfile(metadata, file);
159 
160         String key = getMetadataKey(repository, file);
161 
162         writeLastUpdated(touchfile, key, null);
163     }
164 
165     String getMetadataKey(ArtifactRepository repository, File file) {
166         return repository.getId() + '.' + file.getName() + LAST_UPDATE_TAG;
167     }
168 
169     String getRepositoryKey(ArtifactRepository repository) {
170         StringBuilder buffer = new StringBuilder(256);
171 
172         Proxy proxy = repository.getProxy();
173         if (proxy != null) {
174             if (proxy.getUserName() != null) {
175                 int hash = (proxy.getUserName() + proxy.getPassword()).hashCode();
176                 buffer.append(hash).append('@');
177             }
178             buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>');
179         }
180 
181         // consider the username&password because a repo manager might block artifacts depending on authorization
182         Authentication auth = repository.getAuthentication();
183         if (auth != null) {
184             int hash = (auth.getUsername() + auth.getPassword()).hashCode();
185             buffer.append(hash).append('@');
186         }
187 
188         // consider the URL (instead of the id) as this most closely relates to the contents in the repo
189         buffer.append(repository.getUrl());
190 
191         return buffer.toString();
192     }
193 
194     private void writeLastUpdated(File touchfile, String key, String error) {
195         synchronized (touchfile.getAbsolutePath().intern()) {
196             if (!touchfile.getParentFile().exists()
197                     && !touchfile.getParentFile().mkdirs()) {
198                 getLogger()
199                         .debug("Failed to create directory: " + touchfile.getParent()
200                                 + " for tracking artifact metadata resolution.");
201                 return;
202             }
203 
204             FileChannel channel = null;
205             FileLock lock = null;
206             try {
207                 Properties props = new Properties();
208 
209                 channel = new RandomAccessFile(touchfile, "rw").getChannel();
210                 lock = channel.lock();
211 
212                 if (touchfile.canRead()) {
213                     getLogger().debug("Reading resolution-state from: " + touchfile);
214                     props.load(Channels.newInputStream(channel));
215                 }
216 
217                 props.setProperty(key, Long.toString(System.currentTimeMillis()));
218 
219                 if (error != null) {
220                     props.setProperty(key + ERROR_KEY_SUFFIX, error);
221                 } else {
222                     props.remove(key + ERROR_KEY_SUFFIX);
223                 }
224 
225                 getLogger().debug("Writing resolution-state to: " + touchfile);
226                 channel.truncate(0);
227                 props.store(Channels.newOutputStream(channel), "Last modified on: " + new Date());
228 
229                 lock.release();
230                 lock = null;
231 
232                 channel.close();
233                 channel = null;
234             } catch (IOException e) {
235                 getLogger()
236                         .debug(
237                                 "Failed to record lastUpdated information for resolution.\nFile: "
238                                         + touchfile.toString() + "; key: " + key,
239                                 e);
240             } finally {
241                 if (lock != null) {
242                     try {
243                         lock.release();
244                     } catch (IOException e) {
245                         getLogger()
246                                 .debug("Error releasing exclusive lock for resolution tracking file: " + touchfile, e);
247                     }
248                 }
249 
250                 if (channel != null) {
251                     try {
252                         channel.close();
253                     } catch (IOException e) {
254                         getLogger().debug("Error closing FileChannel for resolution tracking file: " + touchfile, e);
255                     }
256                 }
257             }
258         }
259     }
260 
261     Date readLastUpdated(File touchfile, String key) {
262         getLogger().debug("Searching for " + key + " in resolution tracking file.");
263 
264         Properties props = read(touchfile);
265         if (props != null) {
266             String rawVal = props.getProperty(key);
267             if (rawVal != null) {
268                 try {
269                     return new Date(Long.parseLong(rawVal));
270                 } catch (NumberFormatException e) {
271                     getLogger().debug("Cannot parse lastUpdated date: \'" + rawVal + "\'. Ignoring.", e);
272                 }
273             }
274         }
275         return null;
276     }
277 
278     private String getError(File touchFile, String key) {
279         Properties props = read(touchFile);
280         if (props != null) {
281             return props.getProperty(key + ERROR_KEY_SUFFIX);
282         }
283         return null;
284     }
285 
286     private Properties read(File touchfile) {
287         if (!touchfile.canRead()) {
288             getLogger().debug("Skipped unreadable resolution tracking file " + touchfile);
289             return null;
290         }
291 
292         synchronized (touchfile.getAbsolutePath().intern()) {
293             FileInputStream in = null;
294             FileLock lock = null;
295 
296             try {
297                 Properties props = new Properties();
298 
299                 in = new FileInputStream(touchfile);
300                 lock = in.getChannel().lock(0, Long.MAX_VALUE, true);
301 
302                 getLogger().debug("Reading resolution-state from: " + touchfile);
303                 props.load(in);
304 
305                 lock.release();
306                 lock = null;
307 
308                 in.close();
309                 in = null;
310 
311                 return props;
312             } catch (IOException e) {
313                 getLogger().debug("Failed to read resolution tracking file " + touchfile, e);
314 
315                 return null;
316             } finally {
317                 if (lock != null) {
318                     try {
319                         lock.release();
320                     } catch (IOException e) {
321                         getLogger().debug("Error releasing shared lock for resolution tracking file: " + touchfile, e);
322                     }
323                 }
324 
325                 if (in != null) {
326                     try {
327                         in.close();
328                     } catch (IOException e) {
329                         getLogger().debug("Error closing FileChannel for resolution tracking file: " + touchfile, e);
330                     }
331                 }
332             }
333         }
334     }
335 
336     File getTouchfile(Artifact artifact) {
337         StringBuilder sb = new StringBuilder(128);
338         sb.append(artifact.getArtifactId());
339         sb.append('-').append(artifact.getBaseVersion());
340         if (artifact.getClassifier() != null) {
341             sb.append('-').append(artifact.getClassifier());
342         }
343         sb.append('.').append(artifact.getType()).append(LAST_UPDATE_TAG);
344         return new File(artifact.getFile().getParentFile(), sb.toString());
345     }
346 
347     File getTouchfile(RepositoryMetadata metadata, File file) {
348         return new File(file.getParent(), TOUCHFILE_NAME);
349     }
350 }