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