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.index.updater;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.BufferedInputStream;
26  import java.io.BufferedOutputStream;
27  import java.io.BufferedReader;
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.FileNotFoundException;
31  import java.io.FileOutputStream;
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.InputStreamReader;
35  import java.io.OutputStream;
36  import java.io.OutputStreamWriter;
37  import java.io.Writer;
38  import java.nio.charset.StandardCharsets;
39  import java.nio.file.Files;
40  import java.text.ParseException;
41  import java.text.SimpleDateFormat;
42  import java.util.ArrayList;
43  import java.util.Date;
44  import java.util.List;
45  import java.util.Properties;
46  import java.util.Set;
47  import java.util.TimeZone;
48  
49  import org.apache.lucene.document.Document;
50  import org.apache.lucene.index.DirectoryReader;
51  import org.apache.lucene.index.IndexReader;
52  import org.apache.lucene.index.IndexWriter;
53  import org.apache.lucene.index.IndexWriterConfig;
54  import org.apache.lucene.index.MultiBits;
55  import org.apache.lucene.store.Directory;
56  import org.apache.lucene.util.Bits;
57  import org.apache.maven.index.context.DocumentFilter;
58  import org.apache.maven.index.context.IndexUtils;
59  import org.apache.maven.index.context.IndexingContext;
60  import org.apache.maven.index.context.NexusAnalyzer;
61  import org.apache.maven.index.context.NexusIndexWriter;
62  import org.apache.maven.index.fs.Lock;
63  import org.apache.maven.index.fs.Locker;
64  import org.apache.maven.index.incremental.IncrementalHandler;
65  import org.apache.maven.index.updater.IndexDataReader.IndexDataReadResult;
66  import org.codehaus.plexus.util.FileUtils;
67  import org.codehaus.plexus.util.io.RawInputStreamFacade;
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  /**
72   * A default index updater implementation
73   *
74   * @author Jason van Zyl
75   * @author Eugene Kuleshov
76   */
77  @Singleton
78  @Named
79  public class DefaultIndexUpdater implements IndexUpdater {
80  
81      private final Logger logger = LoggerFactory.getLogger(getClass());
82  
83      protected Logger getLogger() {
84          return logger;
85      }
86  
87      private final IncrementalHandler incrementalHandler;
88  
89      private final List<IndexUpdateSideEffect> sideEffects;
90  
91      @Inject
92      public DefaultIndexUpdater(
93              final IncrementalHandler incrementalHandler, final List<IndexUpdateSideEffect> sideEffects) {
94          this.incrementalHandler = incrementalHandler;
95          this.sideEffects = sideEffects;
96      }
97  
98      public IndexUpdateResult fetchAndUpdateIndex(final IndexUpdateRequest updateRequest) throws IOException {
99          IndexUpdateResult result = new IndexUpdateResult();
100 
101         IndexingContext context = updateRequest.getIndexingContext();
102 
103         ResourceFetcher fetcher = null;
104 
105         if (!updateRequest.isOffline()) {
106             fetcher = updateRequest.getResourceFetcher();
107 
108             // If no resource fetcher passed in, use the wagon fetcher by default
109             // and put back in request for future use
110             if (fetcher == null) {
111                 throw new IOException("Update of the index without provided ResourceFetcher is impossible.");
112             }
113 
114             fetcher.connect(context.getId(), context.getIndexUpdateUrl());
115         }
116 
117         File cacheDir = updateRequest.getLocalIndexCacheDir();
118         Locker locker = updateRequest.getLocker();
119         Lock lock = locker != null && cacheDir != null ? locker.lock(cacheDir) : null;
120         try {
121             if (cacheDir != null) {
122                 LocalCacheIndexAdaptor cache = new LocalCacheIndexAdaptor(cacheDir, result);
123 
124                 if (!updateRequest.isOffline()) {
125                     cacheDir.mkdirs();
126 
127                     try {
128                         if (fetchAndUpdateIndex(updateRequest, fetcher, cache).isSuccessful()) {
129                             cache.commit();
130                         }
131                     } finally {
132                         fetcher.disconnect();
133                     }
134                 }
135 
136                 fetcher = cache.getFetcher();
137             } else if (updateRequest.isOffline()) {
138                 throw new IllegalArgumentException("LocalIndexCacheDir can not be null in offline mode");
139             }
140 
141             try {
142                 if (!updateRequest.isCacheOnly()) {
143                     LuceneIndexAdaptor target = new LuceneIndexAdaptor(updateRequest);
144                     result = fetchAndUpdateIndex(updateRequest, fetcher, target);
145 
146                     if (result.isSuccessful()) {
147                         target.commit();
148                     }
149                 }
150             } finally {
151                 fetcher.disconnect();
152             }
153         } finally {
154             if (lock != null) {
155                 lock.release();
156             }
157         }
158 
159         return result;
160     }
161 
162     private Date loadIndexDirectory(
163             final IndexUpdateRequest updateRequest,
164             final ResourceFetcher fetcher,
165             final boolean merge,
166             final String remoteIndexFile)
167             throws IOException {
168         File indexDir;
169         if (updateRequest.getIndexTempDir() != null) {
170             updateRequest.getIndexTempDir().mkdirs();
171             indexDir = Files.createTempDirectory(updateRequest.getIndexTempDir().toPath(), remoteIndexFile + ".dir")
172                     .toFile();
173         } else {
174             indexDir = Files.createTempDirectory(remoteIndexFile + ".dir").toFile();
175         }
176         try (BufferedInputStream is = new BufferedInputStream(fetcher.retrieve(remoteIndexFile)); //
177                 Directory directory = updateRequest.getFSDirectoryFactory().open(indexDir)) {
178             Date timestamp;
179 
180             Set<String> rootGroups;
181             Set<String> allGroups;
182             if (remoteIndexFile.endsWith(".gz")) {
183                 IndexDataReadResult result =
184                         unpackIndexData(is, updateRequest.getThreads(), directory, updateRequest.getIndexingContext());
185                 timestamp = result.getTimestamp();
186                 rootGroups = result.getRootGroups();
187                 allGroups = result.getAllGroups();
188             } else {
189                 // legacy transfer format
190                 throw new IllegalArgumentException(
191                         "The legacy format is no longer supported " + "by this version of maven-indexer.");
192             }
193 
194             if (updateRequest.getDocumentFilter() != null) {
195                 filterDirectory(directory, updateRequest.getDocumentFilter());
196             }
197 
198             if (merge) {
199                 updateRequest.getIndexingContext().merge(directory);
200             } else {
201                 updateRequest.getIndexingContext().replace(directory, allGroups, rootGroups);
202             }
203             if (sideEffects != null && sideEffects.size() > 0) {
204                 getLogger().info(IndexUpdateSideEffect.class.getName() + " extensions found: " + sideEffects.size());
205                 for (IndexUpdateSideEffect sideeffect : sideEffects) {
206                     sideeffect.updateIndex(directory, updateRequest.getIndexingContext(), merge);
207                 }
208             }
209 
210             return timestamp;
211         } finally {
212             try {
213                 FileUtils.deleteDirectory(indexDir);
214             } catch (IOException ex) {
215                 // ignore
216             }
217         }
218     }
219 
220     private static void filterDirectory(final Directory directory, final DocumentFilter filter) throws IOException {
221         IndexReader r = null;
222         IndexWriter w = null;
223         try {
224             r = DirectoryReader.open(directory);
225             w = new NexusIndexWriter(directory, new IndexWriterConfig(new NexusAnalyzer()));
226 
227             Bits liveDocs = MultiBits.getLiveDocs(r);
228 
229             int numDocs = r.maxDoc();
230 
231             for (int i = 0; i < numDocs; i++) {
232                 if (liveDocs != null && !liveDocs.get(i)) {
233                     continue;
234                 }
235 
236                 Document d = r.document(i);
237 
238                 if (!filter.accept(d)) {
239                     boolean success = w.tryDeleteDocument(r, i) != -1;
240                     // FIXME handle deletion failure
241                 }
242             }
243             w.commit();
244         } finally {
245             IndexUtils.close(r);
246             IndexUtils.close(w);
247         }
248 
249         w = null;
250         try {
251             // analyzer is unimportant, since we are not adding/searching to/on index, only reading/deleting
252             w = new NexusIndexWriter(directory, new IndexWriterConfig(new NexusAnalyzer()));
253 
254             w.commit();
255         } finally {
256             IndexUtils.close(w);
257         }
258     }
259 
260     private Properties loadIndexProperties(final File indexDirectoryFile, final String remoteIndexPropertiesName) {
261         File indexProperties = new File(indexDirectoryFile, remoteIndexPropertiesName);
262 
263         try (FileInputStream fis = new FileInputStream(indexProperties)) {
264             Properties properties = new Properties();
265 
266             properties.load(fis);
267 
268             return properties;
269         } catch (IOException e) {
270             getLogger().debug("Unable to read remote properties stored locally", e);
271         }
272         return null;
273     }
274 
275     private void storeIndexProperties(final File dir, final String indexPropertiesName, final Properties properties)
276             throws IOException {
277         File file = new File(dir, indexPropertiesName);
278 
279         if (properties != null) {
280             try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
281                 properties.store(os, null);
282             }
283         } else {
284             file.delete();
285         }
286     }
287 
288     private Properties downloadIndexProperties(final ResourceFetcher fetcher) throws IOException {
289         try (InputStream fis = fetcher.retrieve(IndexingContext.INDEX_REMOTE_PROPERTIES_FILE)) {
290             Properties properties = new Properties();
291 
292             properties.load(fis);
293 
294             return properties;
295         }
296     }
297 
298     public Date getTimestamp(final Properties properties, final String key) {
299         String indexTimestamp = properties.getProperty(key);
300 
301         if (indexTimestamp != null) {
302             try {
303                 SimpleDateFormat df = new SimpleDateFormat(IndexingContext.INDEX_TIME_FORMAT);
304                 df.setTimeZone(TimeZone.getTimeZone("GMT"));
305                 return df.parse(indexTimestamp);
306             } catch (ParseException ex) {
307             }
308         }
309         return null;
310     }
311 
312     /**
313      * @param is an input stream to unpack index data from
314      * @param threads thread count to use
315      * @param d
316      * @param context
317      */
318     public static IndexDataReadResult unpackIndexData(
319             final InputStream is, final int threads, final Directory d, final IndexingContext context)
320             throws IOException {
321         IndexWriterConfig config = new IndexWriterConfig(new NexusAnalyzer());
322         config.setUseCompoundFile(false);
323         NexusIndexWriter w = new NexusIndexWriter(d, config);
324         try {
325             IndexDataReader dr = new IndexDataReader(is, threads);
326 
327             return dr.readIndex(w, context);
328         } finally {
329             IndexUtils.close(w);
330         }
331     }
332 
333     /**
334      * Filesystem-based ResourceFetcher implementation
335      */
336     public static class FileFetcher implements ResourceFetcher {
337         private final File basedir;
338 
339         public FileFetcher(File basedir) {
340             this.basedir = basedir;
341         }
342 
343         public void connect(String id, String url) throws IOException {
344             // don't need to do anything
345         }
346 
347         public void disconnect() throws IOException {
348             // don't need to do anything
349         }
350 
351         public void retrieve(String name, File targetFile) throws IOException, FileNotFoundException {
352             FileUtils.copyFile(getFile(name), targetFile);
353         }
354 
355         public InputStream retrieve(String name) throws IOException, FileNotFoundException {
356             return new FileInputStream(getFile(name));
357         }
358 
359         private File getFile(String name) {
360             return new File(basedir, name);
361         }
362     }
363 
364     private abstract class IndexAdaptor {
365         protected final File dir;
366 
367         protected Properties properties;
368 
369         protected IndexAdaptor(File dir) {
370             this.dir = dir;
371         }
372 
373         public abstract Properties getProperties();
374 
375         public abstract void storeProperties() throws IOException;
376 
377         public abstract void addIndexChunk(ResourceFetcher source, String filename) throws IOException;
378 
379         public abstract Date setIndexFile(ResourceFetcher source, String string) throws IOException;
380 
381         public Properties setProperties(ResourceFetcher source) throws IOException {
382             this.properties = downloadIndexProperties(source);
383             return properties;
384         }
385 
386         public abstract Date getTimestamp();
387 
388         public void commit() throws IOException {
389             storeProperties();
390         }
391     }
392 
393     private class LuceneIndexAdaptor extends IndexAdaptor {
394         private final IndexUpdateRequest updateRequest;
395 
396         LuceneIndexAdaptor(IndexUpdateRequest updateRequest) {
397             super(updateRequest.getIndexingContext().getIndexDirectoryFile());
398             this.updateRequest = updateRequest;
399         }
400 
401         public Properties getProperties() {
402             if (properties == null) {
403                 properties = loadIndexProperties(dir, IndexingContext.INDEX_UPDATER_PROPERTIES_FILE);
404             }
405             return properties;
406         }
407 
408         public void storeProperties() throws IOException {
409             storeIndexProperties(dir, IndexingContext.INDEX_UPDATER_PROPERTIES_FILE, properties);
410         }
411 
412         public Date getTimestamp() {
413             return updateRequest.getIndexingContext().getTimestamp();
414         }
415 
416         public void addIndexChunk(ResourceFetcher source, String filename) throws IOException {
417             loadIndexDirectory(updateRequest, source, true, filename);
418         }
419 
420         public Date setIndexFile(ResourceFetcher source, String filename) throws IOException {
421             return loadIndexDirectory(updateRequest, source, false, filename);
422         }
423 
424         public void commit() throws IOException {
425             super.commit();
426 
427             updateRequest.getIndexingContext().commit();
428         }
429     }
430 
431     private class LocalCacheIndexAdaptor extends IndexAdaptor {
432         private static final String CHUNKS_FILENAME = "chunks.lst";
433 
434         private final IndexUpdateResult result;
435 
436         private final ArrayList<String> newChunks = new ArrayList<>();
437 
438         LocalCacheIndexAdaptor(File dir, IndexUpdateResult result) {
439             super(dir);
440             this.result = result;
441         }
442 
443         public Properties getProperties() {
444             if (properties == null) {
445                 properties = loadIndexProperties(dir, IndexingContext.INDEX_REMOTE_PROPERTIES_FILE);
446             }
447             return properties;
448         }
449 
450         public void storeProperties() throws IOException {
451             storeIndexProperties(dir, IndexingContext.INDEX_REMOTE_PROPERTIES_FILE, properties);
452         }
453 
454         public Date getTimestamp() {
455             Properties properties = getProperties();
456             if (properties == null) {
457                 return null;
458             }
459 
460             Date timestamp = DefaultIndexUpdater.this.getTimestamp(properties, IndexingContext.INDEX_TIMESTAMP);
461 
462             if (timestamp == null) {
463                 timestamp = DefaultIndexUpdater.this.getTimestamp(properties, IndexingContext.INDEX_LEGACY_TIMESTAMP);
464             }
465 
466             return timestamp;
467         }
468 
469         public void addIndexChunk(ResourceFetcher source, String filename) throws IOException {
470             File chunk = new File(dir, filename);
471             FileUtils.copyStreamToFile(new RawInputStreamFacade(source.retrieve(filename)), chunk);
472             newChunks.add(filename);
473         }
474 
475         public Date setIndexFile(ResourceFetcher source, String filename) throws IOException {
476             cleanCacheDirectory(dir);
477 
478             result.setFullUpdate(true);
479 
480             File target = new File(dir, filename);
481             FileUtils.copyStreamToFile(new RawInputStreamFacade(source.retrieve(filename)), target);
482 
483             return null;
484         }
485 
486         @Override
487         public void commit() throws IOException {
488             File chunksFile = new File(dir, CHUNKS_FILENAME);
489             try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(chunksFile, true)); //
490                     Writer w = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
491                 for (String filename : newChunks) {
492                     w.write(filename + "\n");
493                 }
494                 w.flush();
495             }
496             super.commit();
497         }
498 
499         public List<String> getChunks() throws IOException {
500             ArrayList<String> chunks = new ArrayList<>();
501 
502             File chunksFile = new File(dir, CHUNKS_FILENAME);
503             try (BufferedReader r = new BufferedReader(
504                     new InputStreamReader(new FileInputStream(chunksFile), StandardCharsets.UTF_8))) {
505                 String str;
506                 while ((str = r.readLine()) != null) {
507                     chunks.add(str);
508                 }
509             }
510             return chunks;
511         }
512 
513         public ResourceFetcher getFetcher() {
514             return new LocalIndexCacheFetcher(dir) {
515                 @Override
516                 public List<String> getChunks() throws IOException {
517                     return LocalCacheIndexAdaptor.this.getChunks();
518                 }
519             };
520         }
521     }
522 
523     abstract static class LocalIndexCacheFetcher extends FileFetcher {
524         LocalIndexCacheFetcher(File basedir) {
525             super(basedir);
526         }
527 
528         public abstract List<String> getChunks() throws IOException;
529     }
530 
531     private IndexUpdateResult fetchAndUpdateIndex(
532             final IndexUpdateRequest updateRequest, ResourceFetcher source, IndexAdaptor target) throws IOException {
533         IndexUpdateResult result = new IndexUpdateResult();
534 
535         if (!updateRequest.isForceFullUpdate()) {
536             Properties localProperties = target.getProperties();
537             Date localTimestamp = null;
538 
539             if (localProperties != null) {
540                 localTimestamp = getTimestamp(localProperties, IndexingContext.INDEX_TIMESTAMP);
541             }
542 
543             // this will download and store properties in the target, so next run
544             // target.getProperties() will retrieve it
545             Properties remoteProperties = target.setProperties(source);
546 
547             Date updateTimestamp = getTimestamp(remoteProperties, IndexingContext.INDEX_TIMESTAMP);
548 
549             // If new timestamp is missing, dont bother checking incremental, we have an old file
550             if (updateTimestamp != null) {
551                 List<String> filenames = incrementalHandler.loadRemoteIncrementalUpdates(
552                         updateRequest, localProperties, remoteProperties);
553 
554                 // if we have some incremental files, merge them in
555                 if (filenames != null) {
556                     for (String filename : filenames) {
557                         target.addIndexChunk(source, filename);
558                     }
559 
560                     result.setTimestamp(updateTimestamp);
561                     result.setSuccessful(true);
562                     return result;
563                 }
564             } else {
565                 updateTimestamp = getTimestamp(remoteProperties, IndexingContext.INDEX_LEGACY_TIMESTAMP);
566             }
567 
568             // fallback to timestamp comparison, but try with one coming from local properties, and if not possible (is
569             // null)
570             // fallback to context timestamp
571             if (localTimestamp != null) {
572                 // if we have localTimestamp
573                 // if incremental can't be done for whatever reason, simply use old logic of
574                 // checking the timestamp, if the same, nothing to do
575                 if (updateTimestamp != null && localTimestamp != null && !updateTimestamp.after(localTimestamp)) {
576                     // Index is up to date
577                     result.setSuccessful(true);
578                     return result;
579                 }
580             }
581         } else {
582             // create index properties during forced full index download
583             target.setProperties(source);
584         }
585 
586         if (!updateRequest.isIncrementalOnly()) {
587             Date timestamp;
588             try {
589                 timestamp = target.setIndexFile(source, IndexingContext.INDEX_FILE_PREFIX + ".gz");
590                 if (source instanceof LocalIndexCacheFetcher) {
591                     // local cache has inverse organization compared to remote indexes,
592                     // i.e. initial index file and delta chunks to apply on top of it
593                     for (String filename : ((LocalIndexCacheFetcher) source).getChunks()) {
594                         target.addIndexChunk(source, filename);
595                     }
596                 }
597             } catch (IOException ex) {
598                 // try to look for legacy index transfer format
599                 try {
600                     timestamp = target.setIndexFile(source, IndexingContext.INDEX_FILE_PREFIX + ".zip");
601                 } catch (IOException ex2) {
602                     getLogger().error("Fallback to *.zip also failed: " + ex2); // do not bother with stack trace
603 
604                     throw ex; // original exception more likely to be interesting
605                 }
606             }
607 
608             result.setTimestamp(timestamp);
609             result.setSuccessful(true);
610             result.setFullUpdate(true);
611         }
612 
613         return result;
614     }
615 
616     /**
617      * Cleans specified cache directory. If present, Locker.LOCK_FILE will not be deleted.
618      */
619     protected void cleanCacheDirectory(File dir) throws IOException {
620         File[] members = dir.listFiles();
621         if (members == null) {
622             return;
623         }
624 
625         for (File member : members) {
626             if (!Locker.LOCK_FILE.equals(member.getName())) {
627                 FileUtils.forceDelete(member);
628             }
629         }
630     }
631 }