View Javadoc
1   package org.apache.maven.index.updater;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0    
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import javax.inject.Inject;
23  import javax.inject.Named;
24  import javax.inject.Singleton;
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
80      implements IndexUpdater
81  {
82  
83      private final Logger logger = LoggerFactory.getLogger( getClass() );
84  
85      protected Logger getLogger()
86      {
87          return logger;
88      }
89  
90      private final IncrementalHandler incrementalHandler;
91  
92      private final List<IndexUpdateSideEffect> sideEffects;
93  
94  
95      @Inject
96      public DefaultIndexUpdater( final IncrementalHandler incrementalHandler,
97                                  final List<IndexUpdateSideEffect> sideEffects )
98      {
99          this.incrementalHandler = incrementalHandler;
100         this.sideEffects = sideEffects;
101     }
102 
103     public IndexUpdateResult fetchAndUpdateIndex( final IndexUpdateRequest updateRequest )
104         throws IOException
105     {
106         IndexUpdateResult result = new IndexUpdateResult();
107 
108         IndexingContext context = updateRequest.getIndexingContext();
109 
110         ResourceFetcher fetcher = null;
111 
112         if ( !updateRequest.isOffline() )
113         {
114             fetcher = updateRequest.getResourceFetcher();
115 
116             // If no resource fetcher passed in, use the wagon fetcher by default
117             // and put back in request for future use
118             if ( fetcher == null )
119             {
120                 throw new IOException( "Update of the index without provided ResourceFetcher is impossible." );
121             }
122 
123             fetcher.connect( context.getId(), context.getIndexUpdateUrl() );
124         }
125 
126         File cacheDir = updateRequest.getLocalIndexCacheDir();
127         Locker locker = updateRequest.getLocker();
128         Lock lock = locker != null && cacheDir != null ? locker.lock( cacheDir ) : null;
129         try
130         {
131             if ( cacheDir != null )
132             {
133                 LocalCacheIndexAdaptor cache = new LocalCacheIndexAdaptor( cacheDir, result );
134 
135                 if ( !updateRequest.isOffline() )
136                 {
137                     cacheDir.mkdirs();
138 
139                     try
140                     {
141                         if ( fetchAndUpdateIndex( updateRequest, fetcher, cache ).isSuccessful() )
142                         {
143                             cache.commit();
144                         }
145                     }
146                     finally
147                     {
148                         fetcher.disconnect();
149                     }
150                 }
151 
152                 fetcher = cache.getFetcher();
153             }
154             else if ( updateRequest.isOffline() )
155             {
156                 throw new IllegalArgumentException( "LocalIndexCacheDir can not be null in offline mode" );
157             }
158 
159             try
160             {
161                 if ( !updateRequest.isCacheOnly() )
162                 {
163                     LuceneIndexAdaptor target = new LuceneIndexAdaptor( updateRequest );
164                     result = fetchAndUpdateIndex( updateRequest, fetcher, target );
165                     
166                     if ( result.isSuccessful() )
167                     {
168                         target.commit();
169                     }
170                 }
171             }
172             finally
173             {
174                 fetcher.disconnect();
175             }
176         }
177         finally
178         {
179             if ( lock != null )
180             {
181                 lock.release();
182             }
183         }
184 
185         return result;
186     }
187 
188     private Date loadIndexDirectory( final IndexUpdateRequest updateRequest, final ResourceFetcher fetcher,
189                                      final boolean merge, final String remoteIndexFile )
190         throws IOException
191     {
192         File indexDir;
193         if ( updateRequest.getIndexTempDir() != null )
194         {
195             updateRequest.getIndexTempDir().mkdirs();
196             indexDir = Files.createTempDirectory( updateRequest.getIndexTempDir().toPath(),
197                 remoteIndexFile + ".dir" ).toFile();
198         }
199         else
200         {
201             indexDir = Files.createTempDirectory( remoteIndexFile + ".dir" ).toFile();
202         }
203         try ( BufferedInputStream is = new BufferedInputStream( fetcher.retrieve( remoteIndexFile ) ); //
204                         Directory directory = updateRequest.getFSDirectoryFactory().open( indexDir ) )
205         {
206             Date timestamp;
207 
208             Set<String> rootGroups;
209             Set<String> allGroups;
210             if ( remoteIndexFile.endsWith( ".gz" ) )
211             {
212                 IndexDataReadResult result = unpackIndexData( is, updateRequest.getThreads(), directory,
213                         updateRequest.getIndexingContext() );
214                 timestamp = result.getTimestamp();
215                 rootGroups = result.getRootGroups();
216                 allGroups = result.getAllGroups();
217             }
218             else
219             {
220                 // legacy transfer format
221                 throw new IllegalArgumentException( "The legacy format is no longer supported "
222                     + "by this version of maven-indexer." );
223             }
224 
225             if ( updateRequest.getDocumentFilter() != null )
226             {
227                 filterDirectory( directory, updateRequest.getDocumentFilter() );
228             }
229 
230             if ( merge )
231             {
232                 updateRequest.getIndexingContext().merge( directory );
233             }
234             else
235             {
236                 updateRequest.getIndexingContext().replace( directory, allGroups, rootGroups );
237             }
238             if ( sideEffects != null && sideEffects.size() > 0 )
239             {
240                 getLogger().info( IndexUpdateSideEffect.class.getName() + " extensions found: " + sideEffects.size() );
241                 for ( IndexUpdateSideEffect sideeffect : sideEffects )
242                 {
243                     sideeffect.updateIndex( directory, updateRequest.getIndexingContext(), merge );
244                 }
245             }
246 
247             return timestamp;
248         }
249         finally
250         {
251             try
252             {
253                 FileUtils.deleteDirectory( indexDir );
254             }
255             catch ( IOException ex )
256             {
257                 // ignore
258             }
259         }
260     }
261 
262     private static void filterDirectory( final Directory directory, final DocumentFilter filter )
263         throws IOException
264     {
265         IndexReader r = null;
266         IndexWriter w = null;
267         try
268         {
269             r = DirectoryReader.open( directory );
270             w = new NexusIndexWriter( directory, new IndexWriterConfig( new NexusAnalyzer() ) );
271 
272             Bits liveDocs = MultiBits.getLiveDocs( r );
273 
274             int numDocs = r.maxDoc();
275 
276             for ( int i = 0; i < numDocs; i++ )
277             {
278                 if ( liveDocs != null && !liveDocs.get( i ) )
279                 {
280                     continue;
281                 }
282 
283                 Document d = r.document( i );
284 
285                 if ( !filter.accept( d ) )
286                 {
287                     boolean success = w.tryDeleteDocument( r, i ) != -1;
288                     // FIXME handle deletion failure
289                 }
290             }
291             w.commit();
292         }
293         finally
294         {
295             IndexUtils.close( r );
296             IndexUtils.close( w );
297         }
298 
299         w = null;
300         try
301         {
302             // analyzer is unimportant, since we are not adding/searching to/on index, only reading/deleting
303             w = new NexusIndexWriter( directory, new IndexWriterConfig( new NexusAnalyzer() ) );
304 
305             w.commit();
306         }
307         finally
308         {
309             IndexUtils.close( w );
310         }
311     }
312 
313     private Properties loadIndexProperties( final File indexDirectoryFile, final String remoteIndexPropertiesName )
314     {
315         File indexProperties = new File( indexDirectoryFile, remoteIndexPropertiesName );
316 
317         try ( FileInputStream fis = new FileInputStream( indexProperties ) )
318         {
319             Properties properties = new Properties();
320 
321             properties.load( fis );
322 
323             return properties;
324         }
325         catch ( IOException e )
326         {
327             getLogger().debug( "Unable to read remote properties stored locally", e );
328         }
329         return null;
330     }
331 
332     private void storeIndexProperties( final File dir, final String indexPropertiesName, final Properties properties )
333         throws IOException
334     {
335         File file = new File( dir, indexPropertiesName );
336 
337         if ( properties != null )
338         {
339             try ( OutputStream os = new BufferedOutputStream( new FileOutputStream( file ) ) )
340             {
341                 properties.store( os, null );
342             }
343         }
344         else
345         {
346             file.delete();
347         }
348     }
349 
350     private Properties downloadIndexProperties( final ResourceFetcher fetcher )
351         throws IOException
352     {
353         try ( InputStream fis = fetcher.retrieve( IndexingContext.INDEX_REMOTE_PROPERTIES_FILE ) )
354         {
355             Properties properties = new Properties();
356 
357             properties.load( fis );
358 
359             return properties;
360         }
361     }
362 
363     public Date getTimestamp( final Properties properties, final String key )
364     {
365         String indexTimestamp = properties.getProperty( key );
366 
367         if ( indexTimestamp != null )
368         {
369             try
370             {
371                 SimpleDateFormat df = new SimpleDateFormat( IndexingContext.INDEX_TIME_FORMAT );
372                 df.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
373                 return df.parse( indexTimestamp );
374             }
375             catch ( ParseException ex )
376             {
377             }
378         }
379         return null;
380     }
381 
382     /**
383      * @param is an input stream to unpack index data from
384      * @param threads thread count to use
385      * @param d
386      * @param context
387      */
388     public static IndexDataReadResult unpackIndexData( final InputStream is, final int threads, final Directory d,
389                                                        final IndexingContext context )
390         throws IOException
391     {
392         IndexWriterConfig config = new IndexWriterConfig( new NexusAnalyzer() );
393         config.setUseCompoundFile( false );
394         NexusIndexWriter w = new NexusIndexWriter( d, config );
395         try
396         {
397             IndexDataReader dr = new IndexDataReader( is, threads );
398 
399             return dr.readIndex( w, context );
400         }
401         finally
402         {
403             IndexUtils.close( w );
404         }
405     }
406 
407     /**
408      * Filesystem-based ResourceFetcher implementation
409      */
410     public static class FileFetcher
411         implements ResourceFetcher
412     {
413         private final File basedir;
414 
415         public FileFetcher( File basedir )
416         {
417             this.basedir = basedir;
418         }
419 
420         public void connect( String id, String url )
421             throws IOException
422         {
423             // don't need to do anything
424         }
425 
426         public void disconnect()
427             throws IOException
428         {
429             // don't need to do anything
430         }
431 
432         public void retrieve( String name, File targetFile )
433             throws IOException, FileNotFoundException
434         {
435             FileUtils.copyFile( getFile( name ), targetFile );
436 
437         }
438 
439         public InputStream retrieve( String name )
440             throws IOException, FileNotFoundException
441         {
442             return new FileInputStream( getFile( name ) );
443         }
444 
445         private File getFile( String name )
446         {
447             return new File( basedir, name );
448         }
449 
450     }
451 
452     private abstract class IndexAdaptor
453     {
454         protected final File dir;
455 
456         protected Properties properties;
457 
458         protected IndexAdaptor( File dir )
459         {
460             this.dir = dir;
461         }
462 
463         public abstract Properties getProperties();
464 
465         public abstract void storeProperties()
466             throws IOException;
467 
468         public abstract void addIndexChunk( ResourceFetcher source, String filename )
469             throws IOException;
470 
471         public abstract Date setIndexFile( ResourceFetcher source, String string )
472             throws IOException;
473 
474         public Properties setProperties( ResourceFetcher source )
475             throws IOException
476         {
477             this.properties = downloadIndexProperties( source );
478             return properties;
479         }
480 
481         public abstract Date getTimestamp();
482 
483         public void commit()
484             throws IOException
485         {
486             storeProperties();
487         }
488     }
489 
490     private class LuceneIndexAdaptor
491         extends IndexAdaptor
492     {
493         private final IndexUpdateRequest updateRequest;
494 
495         LuceneIndexAdaptor( IndexUpdateRequest updateRequest )
496         {
497             super( updateRequest.getIndexingContext().getIndexDirectoryFile() );
498             this.updateRequest = updateRequest;
499         }
500 
501         public Properties getProperties()
502         {
503             if ( properties == null )
504             {
505                 properties = loadIndexProperties( dir, IndexingContext.INDEX_UPDATER_PROPERTIES_FILE );
506             }
507             return properties;
508         }
509 
510         public void storeProperties()
511             throws IOException
512         {
513             storeIndexProperties( dir, IndexingContext.INDEX_UPDATER_PROPERTIES_FILE, properties );
514         }
515 
516         public Date getTimestamp()
517         {
518             return updateRequest.getIndexingContext().getTimestamp();
519         }
520 
521         public void addIndexChunk( ResourceFetcher source, String filename )
522             throws IOException
523         {
524             loadIndexDirectory( updateRequest, source, true, filename );
525         }
526 
527         public Date setIndexFile( ResourceFetcher source, String filename )
528             throws IOException
529         {
530             return loadIndexDirectory( updateRequest, source, false, filename );
531         }
532 
533         public void commit()
534             throws IOException
535         {
536             super.commit();
537 
538             updateRequest.getIndexingContext().commit();
539         }
540 
541     }
542 
543     private class LocalCacheIndexAdaptor
544         extends IndexAdaptor
545     {
546         private static final String CHUNKS_FILENAME = "chunks.lst";
547 
548         private final IndexUpdateResult result;
549 
550         private final ArrayList<String> newChunks = new ArrayList<>();
551 
552         LocalCacheIndexAdaptor( File dir, IndexUpdateResult result )
553         {
554             super( dir );
555             this.result = result;
556         }
557 
558         public Properties getProperties()
559         {
560             if ( properties == null )
561             {
562                 properties = loadIndexProperties( dir, IndexingContext.INDEX_REMOTE_PROPERTIES_FILE );
563             }
564             return properties;
565         }
566 
567         public void storeProperties()
568             throws IOException
569         {
570             storeIndexProperties( dir, IndexingContext.INDEX_REMOTE_PROPERTIES_FILE, properties );
571         }
572 
573         public Date getTimestamp()
574         {
575             Properties properties = getProperties();
576             if ( properties == null )
577             {
578                 return null;
579             }
580 
581             Date timestamp = DefaultIndexUpdater.this.getTimestamp( properties, IndexingContext.INDEX_TIMESTAMP );
582 
583             if ( timestamp == null )
584             {
585                 timestamp = DefaultIndexUpdater.this.getTimestamp( properties, IndexingContext.INDEX_LEGACY_TIMESTAMP );
586             }
587 
588             return timestamp;
589         }
590 
591         public void addIndexChunk( ResourceFetcher source, String filename )
592             throws IOException
593         {
594             File chunk = new File( dir, filename );
595             FileUtils.copyStreamToFile( new RawInputStreamFacade( source.retrieve( filename ) ), chunk );
596             newChunks.add( filename );
597         }
598 
599         public Date setIndexFile( ResourceFetcher source, String filename )
600             throws IOException
601         {
602             cleanCacheDirectory( dir );
603 
604             result.setFullUpdate( true );
605 
606             File target = new File( dir, filename );
607             FileUtils.copyStreamToFile( new RawInputStreamFacade( source.retrieve( filename ) ), target );
608 
609             return null;
610         }
611 
612         @Override
613         public void commit()
614             throws IOException
615         {
616             File chunksFile = new File( dir, CHUNKS_FILENAME );
617             try ( BufferedOutputStream os = new BufferedOutputStream( new FileOutputStream( chunksFile, true ) ); //
618                             Writer w = new OutputStreamWriter( os, StandardCharsets.UTF_8 ) )
619             {
620                 for ( String filename : newChunks )
621                 {
622                     w.write( filename + "\n" );
623                 }
624                 w.flush();
625             }
626             super.commit();
627         }
628 
629         public List<String> getChunks()
630             throws IOException
631         {
632             ArrayList<String> chunks = new ArrayList<>();
633 
634             File chunksFile = new File( dir, CHUNKS_FILENAME );
635             try ( BufferedReader r =
636                 new BufferedReader( new InputStreamReader(
637                         new FileInputStream( chunksFile ), StandardCharsets.UTF_8 ) ) )
638             {
639                 String str;
640                 while ( ( str = r.readLine() ) != null )
641                 {
642                     chunks.add( str );
643                 }
644             }
645             return chunks;
646         }
647 
648         public ResourceFetcher getFetcher()
649         {
650             return new LocalIndexCacheFetcher( dir )
651             {
652                 @Override
653                 public List<String> getChunks()
654                     throws IOException
655                 {
656                     return LocalCacheIndexAdaptor.this.getChunks();
657                 }
658             };
659         }
660     }
661 
662     abstract static class LocalIndexCacheFetcher
663         extends FileFetcher
664     {
665         LocalIndexCacheFetcher( File basedir )
666         {
667             super( basedir );
668         }
669 
670         public abstract List<String> getChunks()
671             throws IOException;
672     }
673 
674     private IndexUpdateResult fetchAndUpdateIndex( final IndexUpdateRequest updateRequest, ResourceFetcher source,
675                                       IndexAdaptor target )
676         throws IOException
677     {
678         IndexUpdateResult result = new IndexUpdateResult();
679         
680         if ( !updateRequest.isForceFullUpdate() )
681         {
682             Properties localProperties = target.getProperties();
683             Date localTimestamp = null;
684 
685             if ( localProperties != null )
686             {
687                 localTimestamp = getTimestamp( localProperties, IndexingContext.INDEX_TIMESTAMP );
688             }
689 
690             // this will download and store properties in the target, so next run
691             // target.getProperties() will retrieve it
692             Properties remoteProperties = target.setProperties( source );
693 
694             Date updateTimestamp = getTimestamp( remoteProperties, IndexingContext.INDEX_TIMESTAMP );
695 
696             // If new timestamp is missing, dont bother checking incremental, we have an old file
697             if ( updateTimestamp != null )
698             {
699                 List<String> filenames =
700                     incrementalHandler.loadRemoteIncrementalUpdates( updateRequest, localProperties, remoteProperties );
701 
702                 // if we have some incremental files, merge them in
703                 if ( filenames != null )
704                 {
705                     for ( String filename : filenames )
706                     {
707                         target.addIndexChunk( source, filename );
708                     }
709 
710                     result.setTimestamp( updateTimestamp );
711                     result.setSuccessful( true );
712                     return result;
713                 }
714             }
715             else
716             {
717                 updateTimestamp = getTimestamp( remoteProperties, IndexingContext.INDEX_LEGACY_TIMESTAMP );
718             }
719 
720             // fallback to timestamp comparison, but try with one coming from local properties, and if not possible (is
721             // null)
722             // fallback to context timestamp
723             if ( localTimestamp != null )
724             {
725                 // if we have localTimestamp
726                 // if incremental can't be done for whatever reason, simply use old logic of
727                 // checking the timestamp, if the same, nothing to do
728                 if ( updateTimestamp != null && localTimestamp != null && !updateTimestamp.after( localTimestamp ) )
729                 {
730                     //Index is up to date
731                     result.setSuccessful( true );
732                     return result;
733                 }
734             }
735         }
736         else
737         {
738             // create index properties during forced full index download
739             target.setProperties( source );
740         }
741 
742         if ( !updateRequest.isIncrementalOnly() )
743         {
744             Date timestamp;
745             try
746             {
747                 timestamp = target.setIndexFile( source, IndexingContext.INDEX_FILE_PREFIX + ".gz" );
748                 if ( source instanceof LocalIndexCacheFetcher )
749                 {
750                     // local cache has inverse organization compared to remote indexes,
751                     // i.e. initial index file and delta chunks to apply on top of it
752                     for ( String filename : ( (LocalIndexCacheFetcher) source ).getChunks() )
753                     {
754                         target.addIndexChunk( source, filename );
755                     }
756                 }
757             }
758             catch ( IOException ex )
759             {
760                 // try to look for legacy index transfer format
761                 try
762                 {
763                     timestamp = target.setIndexFile( source, IndexingContext.INDEX_FILE_PREFIX + ".zip" );
764                 }
765                 catch ( IOException ex2 )
766                 {
767                     getLogger().error( "Fallback to *.zip also failed: " + ex2 ); // do not bother with stack trace
768                     
769                     throw ex; // original exception more likely to be interesting
770                 }
771             }
772             
773             result.setTimestamp( timestamp );
774             result.setSuccessful( true );
775             result.setFullUpdate( true );
776         }
777         
778         return result;
779     }
780 
781     /**
782      * Cleans specified cache directory. If present, Locker.LOCK_FILE will not be deleted.
783      */
784     protected void cleanCacheDirectory( File dir )
785         throws IOException
786     {
787         File[] members = dir.listFiles();
788         if ( members == null )
789         {
790             return;
791         }
792 
793         for ( File member : members )
794         {
795             if ( !Locker.LOCK_FILE.equals( member.getName() ) )
796             {
797                 FileUtils.forceDelete( member );
798             }
799         }
800     }
801 
802 }