View Javadoc

1   package org.apache.maven.index.context;
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 java.io.File;
23  import java.io.IOException;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.Date;
28  import java.util.LinkedHashSet;
29  import java.util.List;
30  import java.util.Set;
31  
32  import org.apache.lucene.analysis.Analyzer;
33  import org.apache.lucene.document.Document;
34  import org.apache.lucene.document.Field;
35  import org.apache.lucene.index.CorruptIndexException;
36  import org.apache.lucene.index.IndexFileNameFilter;
37  import org.apache.lucene.index.IndexReader;
38  import org.apache.lucene.index.IndexWriter;
39  import org.apache.lucene.index.IndexWriterConfig;
40  import org.apache.lucene.index.Term;
41  import org.apache.lucene.search.IndexSearcher;
42  import org.apache.lucene.search.SearcherManager;
43  import org.apache.lucene.search.TermQuery;
44  import org.apache.lucene.search.TopDocs;
45  import org.apache.lucene.search.TopScoreDocCollector;
46  import org.apache.lucene.store.Directory;
47  import org.apache.lucene.store.FSDirectory;
48  import org.apache.maven.index.ArtifactInfo;
49  import org.apache.maven.index.artifact.GavCalculator;
50  import org.apache.maven.index.artifact.M2GavCalculator;
51  import org.codehaus.plexus.util.StringUtils;
52  
53  /**
54   * The default {@link IndexingContext} implementation.
55   * 
56   * @author Jason van Zyl
57   * @author Tamas Cservenak
58   */
59  public class DefaultIndexingContext
60      extends AbstractIndexingContext
61  {
62      /**
63       * A standard location for indices served up by a webserver.
64       */
65      private static final String INDEX_DIRECTORY = ".index";
66  
67      public static final String FLD_DESCRIPTOR = "DESCRIPTOR";
68  
69      private static final String FLD_DESCRIPTOR_CONTENTS = "NexusIndex";
70  
71      private static final String FLD_IDXINFO = "IDXINFO";
72  
73      private static final String VERSION = "1.0";
74  
75      private static final Term DESCRIPTOR_TERM = new Term( FLD_DESCRIPTOR, FLD_DESCRIPTOR_CONTENTS );
76  
77      private Directory indexDirectory;
78  
79      private File indexDirectoryFile;
80  
81      private String id;
82  
83      private boolean searchable;
84  
85      private String repositoryId;
86  
87      private File repository;
88  
89      private String repositoryUrl;
90  
91      private String indexUpdateUrl;
92  
93      private NexusIndexWriter indexWriter;
94  
95      private SearcherManager searcherManager;
96  
97      private Date timestamp;
98  
99      private List<? extends IndexCreator> indexCreators;
100 
101     /**
102      * Currently nexus-indexer knows only M2 reposes
103      * <p>
104      * XXX move this into a concrete Scanner implementation
105      */
106     private GavCalculator gavCalculator;
107 
108     private DefaultIndexingContext( String id,
109                                     String repositoryId,
110                                     File repository, //
111                                     String repositoryUrl, String indexUpdateUrl,
112                                     List<? extends IndexCreator> indexCreators, Directory indexDirectory,
113                                     boolean reclaimIndex )
114         throws ExistingLuceneIndexMismatchException, IOException
115     {
116         this.id = id;
117 
118         this.searchable = true;
119 
120         this.repositoryId = repositoryId;
121 
122         this.repository = repository;
123 
124         this.repositoryUrl = repositoryUrl;
125 
126         this.indexUpdateUrl = indexUpdateUrl;
127 
128         this.indexWriter = null;
129 
130         this.searcherManager = null;
131 
132         this.indexCreators = indexCreators;
133 
134         this.indexDirectory = indexDirectory;
135 
136         // eh?
137         // Guice does NOT initialize these, and we have to do manually?
138         // While in Plexus, all is well, but when in guice-shim,
139         // these objects are still LazyHintedBeans or what not and IndexerFields are NOT registered!
140         for ( IndexCreator indexCreator : indexCreators )
141         {
142             indexCreator.getIndexerFields();
143         }
144 
145         this.gavCalculator = new M2GavCalculator();
146 
147         prepareIndex( reclaimIndex );
148     }
149 
150     public DefaultIndexingContext( String id, String repositoryId, File repository, File indexDirectoryFile,
151                                    String repositoryUrl, String indexUpdateUrl,
152                                    List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
153         throws IOException, ExistingLuceneIndexMismatchException
154     {
155         this( id, repositoryId, repository, repositoryUrl, indexUpdateUrl, indexCreators,
156             FSDirectory.open( indexDirectoryFile ), reclaimIndex );
157 
158         this.indexDirectoryFile = indexDirectoryFile;
159     }
160 
161     @Deprecated
162     public DefaultIndexingContext( String id, String repositoryId, File repository, Directory indexDirectory,
163                                    String repositoryUrl, String indexUpdateUrl,
164                                    List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
165         throws IOException, ExistingLuceneIndexMismatchException
166     {
167         this( id, repositoryId, repository, repositoryUrl, indexUpdateUrl, indexCreators, indexDirectory, reclaimIndex );
168 
169         if ( indexDirectory instanceof FSDirectory )
170         {
171             this.indexDirectoryFile = ( (FSDirectory) indexDirectory ).getDirectory();
172         }
173     }
174 
175     public Directory getIndexDirectory()
176     {
177         return indexDirectory;
178     }
179 
180     public File getIndexDirectoryFile()
181     {
182         return indexDirectoryFile;
183     }
184 
185     private void prepareIndex( boolean reclaimIndex )
186         throws IOException, ExistingLuceneIndexMismatchException
187     {
188         if ( IndexReader.indexExists( indexDirectory ) )
189         {
190             try
191             {
192                 // unlock the dir forcibly
193                 if ( IndexWriter.isLocked( indexDirectory ) )
194                 {
195                     IndexWriter.unlock( indexDirectory );
196                 }
197 
198                 openAndWarmup();
199 
200                 checkAndUpdateIndexDescriptor( reclaimIndex );
201             }
202             catch ( IOException e )
203             {
204                 if ( reclaimIndex )
205                 {
206                     prepareCleanIndex( true );
207                 }
208                 else
209                 {
210                     throw e;
211                 }
212             }
213         }
214         else
215         {
216             prepareCleanIndex( false );
217         }
218 
219         timestamp = IndexUtils.getTimestamp( indexDirectory );
220     }
221 
222     private void prepareCleanIndex( boolean deleteExisting )
223         throws IOException
224     {
225         if ( deleteExisting )
226         {
227             closeReaders();
228 
229             // unlock the dir forcibly
230             if ( IndexWriter.isLocked( indexDirectory ) )
231             {
232                 IndexWriter.unlock( indexDirectory );
233             }
234 
235             deleteIndexFiles( true );
236         }
237 
238         openAndWarmup();
239 
240         if ( StringUtils.isEmpty( getRepositoryId() ) )
241         {
242             throw new IllegalArgumentException( "The repositoryId cannot be null when creating new repository!" );
243         }
244 
245         storeDescriptor();
246     }
247 
248     private void checkAndUpdateIndexDescriptor( boolean reclaimIndex )
249         throws IOException, ExistingLuceneIndexMismatchException
250     {
251         if ( reclaimIndex )
252         {
253             // forcefully "reclaiming" the ownership of the index as ours
254             storeDescriptor();
255             return;
256         }
257 
258         // check for descriptor if this is not a "virgin" index
259         if ( getSize() > 0 )
260         {
261             final TopScoreDocCollector collector = TopScoreDocCollector.create( 1, false );
262             final IndexSearcher indexSearcher = acquireIndexSearcher();
263             try
264             {
265                 indexSearcher.search( new TermQuery( DESCRIPTOR_TERM ), collector );
266 
267                 if ( collector.getTotalHits() == 0 )
268                 {
269                     throw new ExistingLuceneIndexMismatchException(
270                         "The existing index has no NexusIndexer descriptor" );
271                 }
272 
273                 if ( collector.getTotalHits() > 1 )
274                 {
275                     // eh? this is buggy index it seems, just iron it out then
276                     storeDescriptor();
277                     return;
278                 }
279                 else
280                 {
281                     // good, we have one descriptor as should
282                     Document descriptor = indexSearcher.doc( collector.topDocs().scoreDocs[0].doc );
283                     String[] h = StringUtils.split( descriptor.get( FLD_IDXINFO ), ArtifactInfo.FS );
284                     // String version = h[0];
285                     String repoId = h[1];
286 
287                     // // compare version
288                     // if ( !VERSION.equals( version ) )
289                     // {
290                     // throw new UnsupportedExistingLuceneIndexException(
291                     // "The existing index has version [" + version + "] and not [" + VERSION + "] version!" );
292                     // }
293 
294                     if ( getRepositoryId() == null )
295                     {
296                         repositoryId = repoId;
297                     }
298                     else if ( !getRepositoryId().equals( repoId ) )
299                     {
300                         throw new ExistingLuceneIndexMismatchException( "The existing index is for repository " //
301                             + "[" + repoId + "] and not for repository [" + getRepositoryId() + "]" );
302                     }
303                 }
304             }
305             finally
306             {
307                 releaseIndexSearcher( indexSearcher );
308             }
309         }
310     }
311 
312     private void storeDescriptor()
313         throws IOException
314     {
315         Document hdr = new Document();
316 
317         hdr.add( new Field( FLD_DESCRIPTOR, FLD_DESCRIPTOR_CONTENTS, Field.Store.YES, Field.Index.NOT_ANALYZED ) );
318 
319         hdr.add( new Field( FLD_IDXINFO, VERSION + ArtifactInfo.FS + getRepositoryId(), Field.Store.YES, Field.Index.NO ) );
320 
321         IndexWriter w = getIndexWriter();
322 
323         w.updateDocument( DESCRIPTOR_TERM, hdr );
324 
325         w.commit();
326     }
327 
328     private void deleteIndexFiles( boolean full )
329         throws IOException
330     {
331         if ( indexDirectory != null )
332         {
333             String[] names = indexDirectory.listAll();
334 
335             if ( names != null )
336             {
337                 IndexFileNameFilter filter = IndexFileNameFilter.getFilter();
338 
339                 for ( int i = 0; i < names.length; i++ )
340                 {
341                     if ( filter.accept( null, names[i] ) )
342                     {
343                         indexDirectory.deleteFile( names[i] );
344                     }
345                 }
346             }
347 
348             if ( full )
349             {
350                 if ( indexDirectory.fileExists( INDEX_PACKER_PROPERTIES_FILE ) )
351                 {
352                     indexDirectory.deleteFile( INDEX_PACKER_PROPERTIES_FILE );
353                 }
354 
355                 if ( indexDirectory.fileExists( INDEX_UPDATER_PROPERTIES_FILE ) )
356                 {
357                     indexDirectory.deleteFile( INDEX_UPDATER_PROPERTIES_FILE );
358                 }
359             }
360 
361             IndexUtils.deleteTimestamp( indexDirectory );
362         }
363     }
364 
365     // ==
366 
367     public boolean isSearchable()
368     {
369         return searchable;
370     }
371 
372     public void setSearchable( boolean searchable )
373     {
374         this.searchable = searchable;
375     }
376 
377     public String getId()
378     {
379         return id;
380     }
381 
382     public void updateTimestamp()
383         throws IOException
384     {
385         updateTimestamp( false );
386     }
387 
388     public void updateTimestamp( boolean save )
389         throws IOException
390     {
391         updateTimestamp( save, new Date() );
392     }
393 
394     public void updateTimestamp( boolean save, Date timestamp )
395         throws IOException
396     {
397         this.timestamp = timestamp;
398 
399         if ( save )
400         {
401             IndexUtils.updateTimestamp( indexDirectory, getTimestamp() );
402         }
403     }
404 
405     public Date getTimestamp()
406     {
407         return timestamp;
408     }
409 
410     public int getSize()
411         throws IOException
412     {
413         final IndexSearcher is = acquireIndexSearcher();
414         try
415         {
416             return is.getIndexReader().numDocs();
417         }
418         finally
419         {
420             releaseIndexSearcher( is );
421         }
422     }
423 
424     public String getRepositoryId()
425     {
426         return repositoryId;
427     }
428 
429     public File getRepository()
430     {
431         return repository;
432     }
433 
434     public String getRepositoryUrl()
435     {
436         return repositoryUrl;
437     }
438 
439     public String getIndexUpdateUrl()
440     {
441         if ( repositoryUrl != null )
442         {
443             if ( indexUpdateUrl == null || indexUpdateUrl.trim().length() == 0 )
444             {
445                 return repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + INDEX_DIRECTORY;
446             }
447         }
448         return indexUpdateUrl;
449     }
450 
451     public Analyzer getAnalyzer()
452     {
453         return new NexusAnalyzer();
454     }
455 
456     protected void openAndWarmup()
457         throws IOException
458     {
459         // IndexWriter (close)
460         if ( indexWriter != null )
461         {
462             indexWriter.close();
463 
464             indexWriter = null;
465         }
466         if ( searcherManager != null )
467         {
468             searcherManager.close();
469 
470             searcherManager = null;
471         }
472 
473         this.indexWriter = new NexusIndexWriter( getIndexDirectory(), getWriterConfig() );
474         this.indexWriter.commit(); // LUCENE-2386
475         this.searcherManager = new SearcherManager( indexWriter, false, new NexusIndexSearcherFactory( this ) );
476     }
477 
478     /**
479      * Returns new IndexWriterConfig instance
480      * 
481      * @since 5.1
482      */
483     protected IndexWriterConfig getWriterConfig()
484     {
485         return NexusIndexWriter.defaultConfig();
486     }
487 
488     public IndexWriter getIndexWriter()
489         throws IOException
490     {
491         return indexWriter;
492     }
493 
494     public IndexSearcher acquireIndexSearcher()
495         throws IOException
496     {
497         // TODO: move this to separate thread to not penalty next incoming searcher
498         searcherManager.maybeRefresh();
499         return searcherManager.acquire();
500     }
501 
502     public void releaseIndexSearcher( final IndexSearcher is )
503         throws IOException
504     {
505         if ( is == null )
506         {
507             return;
508         }
509         searcherManager.release( is );
510     }
511 
512     public void commit()
513         throws IOException
514     {
515         getIndexWriter().commit();
516     }
517 
518     public void rollback()
519         throws IOException
520     {
521         getIndexWriter().rollback();
522     }
523 
524     public synchronized void optimize()
525         throws CorruptIndexException, IOException
526     {
527         getIndexWriter().optimize();
528         commit();
529     }
530 
531     public synchronized void close( boolean deleteFiles )
532         throws IOException
533     {
534         if ( indexDirectory != null )
535         {
536             IndexUtils.updateTimestamp( indexDirectory, getTimestamp() );
537             closeReaders();
538             if ( deleteFiles )
539             {
540                 deleteIndexFiles( true );
541             }
542             indexDirectory.close();
543         }
544         indexDirectory = null;
545     }
546 
547     public synchronized void purge()
548         throws IOException
549     {
550         closeReaders();
551         deleteIndexFiles( true );
552         openAndWarmup();
553         try
554         {
555             prepareIndex( true );
556         }
557         catch ( ExistingLuceneIndexMismatchException e )
558         {
559             // just deleted it
560         }
561         rebuildGroups();
562         updateTimestamp( true, null );
563     }
564 
565     public synchronized void replace( Directory directory )
566         throws IOException
567     {
568         final Date ts = IndexUtils.getTimestamp( directory );
569         closeReaders();
570         deleteIndexFiles( false );
571         IndexUtils.copyDirectory( directory, indexDirectory );
572         openAndWarmup();
573         // reclaim the index as mine
574         storeDescriptor();
575         rebuildGroups();
576         updateTimestamp( true, ts );
577         optimize();
578     }
579 
580     public synchronized void merge( Directory directory )
581         throws IOException
582     {
583         merge( directory, null );
584     }
585 
586     public synchronized void merge( Directory directory, DocumentFilter filter )
587         throws IOException
588     {
589         final IndexSearcher s = acquireIndexSearcher();
590         try
591         {
592             final IndexWriter w = getIndexWriter();
593             final IndexReader directoryReader = IndexReader.open( directory, true );
594             TopScoreDocCollector collector = null;
595             try
596             {
597                 int numDocs = directoryReader.maxDoc();
598                 for ( int i = 0; i < numDocs; i++ )
599                 {
600                     if ( directoryReader.isDeleted( i ) )
601                     {
602                         continue;
603                     }
604 
605                     Document d = directoryReader.document( i );
606                     if ( filter != null && !filter.accept( d ) )
607                     {
608                         continue;
609                     }
610 
611                     String uinfo = d.get( ArtifactInfo.UINFO );
612                     if ( uinfo != null )
613                     {
614                         collector = TopScoreDocCollector.create( 1, false );
615                         s.search( new TermQuery( new Term( ArtifactInfo.UINFO, uinfo ) ), collector );
616                         if ( collector.getTotalHits() == 0 )
617                         {
618                             w.addDocument( IndexUtils.updateDocument( d, this, false ) );
619                         }
620                     }
621                     else
622                     {
623                         String deleted = d.get( ArtifactInfo.DELETED );
624 
625                         if ( deleted != null )
626                         {
627                             // Deleting the document loses history that it was delete,
628                             // so incrementals wont work. Therefore, put the delete
629                             // document in as well
630                             w.deleteDocuments( new Term( ArtifactInfo.UINFO, deleted ) );
631                             w.addDocument( d );
632                         }
633                     }
634                 }
635 
636             }
637             finally
638             {
639                 directoryReader.close();
640                 commit();
641             }
642 
643             rebuildGroups();
644             Date mergedTimestamp = IndexUtils.getTimestamp( directory );
645 
646             if ( getTimestamp() != null && mergedTimestamp != null && mergedTimestamp.after( getTimestamp() ) )
647             {
648                 // we have both, keep the newest
649                 updateTimestamp( true, mergedTimestamp );
650             }
651             else
652             {
653                 updateTimestamp( true );
654             }
655             optimize();
656         }
657         finally
658         {
659             releaseIndexSearcher( s );
660         }
661     }
662 
663     private void closeReaders()
664         throws CorruptIndexException, IOException
665     {
666         if ( searcherManager != null )
667         {
668             searcherManager.close();
669             searcherManager = null;
670         }
671         if ( indexWriter != null )
672         {
673             indexWriter.close();
674             indexWriter = null;
675         }
676     }
677 
678     public GavCalculator getGavCalculator()
679     {
680         return gavCalculator;
681     }
682 
683     public List<IndexCreator> getIndexCreators()
684     {
685         return Collections.unmodifiableList( indexCreators );
686     }
687 
688     // groups
689 
690     public synchronized void rebuildGroups()
691         throws IOException
692     {
693         final IndexSearcher is = acquireIndexSearcher();
694         try
695         {
696             final IndexReader r = is.getIndexReader();
697 
698             Set<String> rootGroups = new LinkedHashSet<String>();
699             Set<String> allGroups = new LinkedHashSet<String>();
700 
701             int numDocs = r.maxDoc();
702 
703             for ( int i = 0; i < numDocs; i++ )
704             {
705                 if ( r.isDeleted( i ) )
706                 {
707                     continue;
708                 }
709 
710                 Document d = r.document( i );
711 
712                 String uinfo = d.get( ArtifactInfo.UINFO );
713 
714                 if ( uinfo != null )
715                 {
716                     ArtifactInfo info = IndexUtils.constructArtifactInfo( d, this );
717                     rootGroups.add( info.getRootGroup() );
718                     allGroups.add( info.groupId );
719                 }
720             }
721 
722             setRootGroups( rootGroups );
723             setAllGroups( allGroups );
724 
725             optimize();
726         }
727         finally
728         {
729             releaseIndexSearcher( is );
730         }
731     }
732 
733     public Set<String> getAllGroups()
734         throws IOException
735     {
736         return getGroups( ArtifactInfo.ALL_GROUPS, ArtifactInfo.ALL_GROUPS_VALUE, ArtifactInfo.ALL_GROUPS_LIST );
737     }
738 
739     public synchronized void setAllGroups( Collection<String> groups )
740         throws IOException
741     {
742         setGroups( groups, ArtifactInfo.ALL_GROUPS, ArtifactInfo.ALL_GROUPS_VALUE, ArtifactInfo.ALL_GROUPS_LIST );
743         commit();
744     }
745 
746     public Set<String> getRootGroups()
747         throws IOException
748     {
749         return getGroups( ArtifactInfo.ROOT_GROUPS, ArtifactInfo.ROOT_GROUPS_VALUE, ArtifactInfo.ROOT_GROUPS_LIST );
750     }
751 
752     public synchronized void setRootGroups( Collection<String> groups )
753         throws IOException
754     {
755         setGroups( groups, ArtifactInfo.ROOT_GROUPS, ArtifactInfo.ROOT_GROUPS_VALUE, ArtifactInfo.ROOT_GROUPS_LIST );
756         commit();
757     }
758 
759     protected Set<String> getGroups( String field, String filedValue, String listField )
760         throws IOException, CorruptIndexException
761     {
762         final TopScoreDocCollector collector = TopScoreDocCollector.create( 1, false );
763         final IndexSearcher indexSearcher = acquireIndexSearcher();
764         try
765         {
766             indexSearcher.search( new TermQuery( new Term( field, filedValue ) ), collector );
767             TopDocs topDocs = collector.topDocs();
768             Set<String> groups = new LinkedHashSet<String>( Math.max( 10, topDocs.totalHits ) );
769             if ( topDocs.totalHits > 0 )
770             {
771                 Document doc = indexSearcher.doc( topDocs.scoreDocs[0].doc );
772                 String groupList = doc.get( listField );
773                 if ( groupList != null )
774                 {
775                     groups.addAll( Arrays.asList( groupList.split( "\\|" ) ) );
776                 }
777             }
778             return groups;
779         }
780         finally
781         {
782             releaseIndexSearcher( indexSearcher );
783         }
784     }
785 
786     protected void setGroups( Collection<String> groups, String groupField, String groupFieldValue,
787                               String groupListField )
788         throws IOException, CorruptIndexException
789     {
790         final IndexWriter w = getIndexWriter();
791         w.updateDocument( new Term( groupField, groupFieldValue ),
792             createGroupsDocument( groups, groupField, groupFieldValue, groupListField ) );
793     }
794 
795     protected Document createGroupsDocument( Collection<String> groups, String field, String fieldValue,
796                                              String listField )
797     {
798         final Document groupDoc = new Document();
799         groupDoc.add( new Field( field, //
800             fieldValue, Field.Store.YES, Field.Index.NOT_ANALYZED ) );
801         groupDoc.add( new Field( listField, //
802             ArtifactInfo.lst2str( groups ), Field.Store.YES, Field.Index.NO ) );
803         return groupDoc;
804     }
805 
806     @Override
807     public String toString()
808     {
809         return id + " : " + timestamp;
810     }
811 }