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