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