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