blob: 8a64a4d65624994a95f8ccf3ca704d1d97815ef7 [file] [log] [blame]
package org.apache.maven.index.context;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.MultiFields;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.FSLockFactory;
import org.apache.lucene.store.Lock;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.Bits;
import org.apache.maven.index.ArtifactInfo;
import org.apache.maven.index.IndexerField;
import org.apache.maven.index.artifact.GavCalculator;
import org.apache.maven.index.artifact.M2GavCalculator;
import org.codehaus.plexus.util.StringUtils;
/**
* The default {@link IndexingContext} implementation.
*
* @author Jason van Zyl
* @author Tamas Cservenak
*/
public class DefaultIndexingContext
extends AbstractIndexingContext
{
/**
* A standard location for indices served up by a webserver.
*/
private static final String INDEX_DIRECTORY = ".index";
public static final String FLD_DESCRIPTOR = "DESCRIPTOR";
public static final String FLD_DESCRIPTOR_CONTENTS = "NexusIndex";
public static final String FLD_IDXINFO = "IDXINFO";
public static final String VERSION = "1.0";
private static final Term DESCRIPTOR_TERM = new Term( FLD_DESCRIPTOR, FLD_DESCRIPTOR_CONTENTS );
private Directory indexDirectory;
private TrackingLockFactory lockFactory;
private File indexDirectoryFile;
private String id;
private boolean searchable;
private String repositoryId;
private File repository;
private String repositoryUrl;
private String indexUpdateUrl;
private NexusIndexWriter indexWriter;
private SearcherManager searcherManager;
private Date timestamp;
private List<? extends IndexCreator> indexCreators;
/**
* Currently nexus-indexer knows only M2 reposes
* <p>
* XXX move this into a concrete Scanner implementation
*/
private GavCalculator gavCalculator;
private DefaultIndexingContext( String id,
String repositoryId,
File repository, //
String repositoryUrl, String indexUpdateUrl,
List<? extends IndexCreator> indexCreators, Directory indexDirectory,
TrackingLockFactory lockFactory,
boolean reclaimIndex )
throws ExistingLuceneIndexMismatchException, IOException
{
this.id = id;
this.searchable = true;
this.repositoryId = repositoryId;
this.repository = repository;
this.repositoryUrl = repositoryUrl;
this.indexUpdateUrl = indexUpdateUrl;
this.indexWriter = null;
this.searcherManager = null;
this.indexCreators = indexCreators;
this.indexDirectory = indexDirectory;
this.lockFactory = lockFactory;
// eh?
// Guice does NOT initialize these, and we have to do manually?
// While in Plexus, all is well, but when in guice-shim,
// these objects are still LazyHintedBeans or what not and IndexerFields are NOT registered!
for ( IndexCreator indexCreator : indexCreators )
{
indexCreator.getIndexerFields();
}
this.gavCalculator = new M2GavCalculator();
prepareIndex( reclaimIndex );
setIndexDirectoryFile( null );
}
private DefaultIndexingContext( String id, String repositoryId, File repository, File indexDirectoryFile,
TrackingLockFactory lockFactory, String repositoryUrl, String indexUpdateUrl,
List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
throws IOException, ExistingLuceneIndexMismatchException
{
this( id, repositoryId, repository, repositoryUrl, indexUpdateUrl, indexCreators,
FSDirectory.open( indexDirectoryFile.toPath(), lockFactory ), lockFactory, reclaimIndex );
setIndexDirectoryFile( indexDirectoryFile );
}
public DefaultIndexingContext( String id, String repositoryId, File repository, File indexDirectoryFile,
String repositoryUrl, String indexUpdateUrl,
List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
throws IOException, ExistingLuceneIndexMismatchException
{
this( id, repositoryId, repository, indexDirectoryFile, new TrackingLockFactory( FSLockFactory.getDefault() ),
repositoryUrl, indexUpdateUrl, indexCreators, reclaimIndex );
}
@Deprecated
public DefaultIndexingContext( String id, String repositoryId, File repository, Directory indexDirectory,
String repositoryUrl, String indexUpdateUrl,
List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
throws IOException, ExistingLuceneIndexMismatchException
{
this( id, repositoryId, repository, repositoryUrl, indexUpdateUrl, indexCreators, indexDirectory, null,
reclaimIndex ); // Lock factory already installed - pass null
if ( indexDirectory instanceof FSDirectory )
{
setIndexDirectoryFile( ( (FSDirectory) indexDirectory ).getDirectory().toFile() );
}
}
public Directory getIndexDirectory()
{
return indexDirectory;
}
/**
* Sets index location. As usually index is persistent (is on disk), this will point to that value, but in
* some circumstances (ie, using RAMDisk for index), this will point to an existing tmp directory.
*/
protected void setIndexDirectoryFile( File dir )
throws IOException
{
if ( dir == null )
{
// best effort, to have a directory thru the life of a ctx
File tmpFile = File.createTempFile( "mindexer-ctx" + id, "tmp" );
tmpFile.deleteOnExit();
tmpFile.delete();
tmpFile.mkdirs();
this.indexDirectoryFile = tmpFile;
}
else
{
this.indexDirectoryFile = dir;
}
}
public File getIndexDirectoryFile()
{
return indexDirectoryFile;
}
private void prepareIndex( boolean reclaimIndex )
throws IOException, ExistingLuceneIndexMismatchException
{
if ( DirectoryReader.indexExists( indexDirectory ) )
{
try
{
// unlock the dir forcibly
if ( IndexWriter.isLocked( indexDirectory ) )
{
unlockForcibly( lockFactory, indexDirectory );
}
openAndWarmup();
checkAndUpdateIndexDescriptor( reclaimIndex );
}
catch ( IOException e )
{
if ( reclaimIndex )
{
prepareCleanIndex( true );
}
else
{
throw e;
}
}
}
else
{
prepareCleanIndex( false );
}
timestamp = IndexUtils.getTimestamp( indexDirectory );
}
private void prepareCleanIndex( boolean deleteExisting )
throws IOException
{
if ( deleteExisting )
{
closeReaders();
// unlock the dir forcibly
if ( IndexWriter.isLocked( indexDirectory ) )
{
unlockForcibly( lockFactory, indexDirectory );
}
deleteIndexFiles( true );
}
openAndWarmup();
if ( StringUtils.isEmpty( getRepositoryId() ) )
{
throw new IllegalArgumentException( "The repositoryId cannot be null when creating new repository!" );
}
storeDescriptor();
}
private void checkAndUpdateIndexDescriptor( boolean reclaimIndex )
throws IOException, ExistingLuceneIndexMismatchException
{
if ( reclaimIndex )
{
// forcefully "reclaiming" the ownership of the index as ours
storeDescriptor();
return;
}
// check for descriptor if this is not a "virgin" index
if ( getSize() > 0 )
{
final TopScoreDocCollector collector = TopScoreDocCollector.create( 1 );
final IndexSearcher indexSearcher = acquireIndexSearcher();
try
{
indexSearcher.search( new TermQuery( DESCRIPTOR_TERM ), collector );
if ( collector.getTotalHits() == 0 )
{
throw new ExistingLuceneIndexMismatchException(
"The existing index has no NexusIndexer descriptor" );
}
if ( collector.getTotalHits() > 1 )
{
// eh? this is buggy index it seems, just iron it out then
storeDescriptor();
return;
}
else
{
// good, we have one descriptor as should
Document descriptor = indexSearcher.doc( collector.topDocs().scoreDocs[0].doc );
String[] h = StringUtils.split( descriptor.get( FLD_IDXINFO ), ArtifactInfo.FS );
// String version = h[0];
String repoId = h[1];
// // compare version
// if ( !VERSION.equals( version ) )
// {
// throw new UnsupportedExistingLuceneIndexException(
// "The existing index has version [" + version + "] and not [" + VERSION + "] version!" );
// }
if ( getRepositoryId() == null )
{
repositoryId = repoId;
}
else if ( !getRepositoryId().equals( repoId ) )
{
throw new ExistingLuceneIndexMismatchException( "The existing index is for repository " //
+ "[" + repoId + "] and not for repository [" + getRepositoryId() + "]" );
}
}
}
finally
{
releaseIndexSearcher( indexSearcher );
}
}
}
private void storeDescriptor()
throws IOException
{
Document hdr = new Document();
hdr.add( new Field( FLD_DESCRIPTOR, FLD_DESCRIPTOR_CONTENTS, IndexerField.KEYWORD_STORED ) );
hdr.add( new StoredField( FLD_IDXINFO, VERSION + ArtifactInfo.FS + getRepositoryId() ) );
IndexWriter w = getIndexWriter();
w.updateDocument( DESCRIPTOR_TERM, hdr );
w.commit();
}
private void deleteIndexFiles( boolean full )
throws IOException
{
if ( indexDirectory != null )
{
String[] names = indexDirectory.listAll();
if ( names != null )
{
for ( String name : names )
{
if ( !( name.equals( INDEX_PACKER_PROPERTIES_FILE )
|| name.equals( INDEX_UPDATER_PROPERTIES_FILE ) ) )
{
indexDirectory.deleteFile( name );
}
}
}
if ( full )
{
try
{
indexDirectory.deleteFile( INDEX_PACKER_PROPERTIES_FILE );
}
catch ( IOException ioe )
{
//Does not exist
}
try
{
indexDirectory.deleteFile( INDEX_UPDATER_PROPERTIES_FILE );
}
catch ( IOException ioe )
{
//Does not exist
}
}
IndexUtils.deleteTimestamp( indexDirectory );
}
}
// ==
public boolean isSearchable()
{
return searchable;
}
public void setSearchable( boolean searchable )
{
this.searchable = searchable;
}
public String getId()
{
return id;
}
public void updateTimestamp()
throws IOException
{
updateTimestamp( false );
}
public void updateTimestamp( boolean save )
throws IOException
{
updateTimestamp( save, new Date() );
}
public void updateTimestamp( boolean save, Date timestamp )
throws IOException
{
this.timestamp = timestamp;
if ( save )
{
IndexUtils.updateTimestamp( indexDirectory, getTimestamp() );
}
}
public Date getTimestamp()
{
return timestamp;
}
public int getSize()
throws IOException
{
final IndexSearcher is = acquireIndexSearcher();
try
{
return is.getIndexReader().numDocs();
}
finally
{
releaseIndexSearcher( is );
}
}
public String getRepositoryId()
{
return repositoryId;
}
public File getRepository()
{
return repository;
}
public String getRepositoryUrl()
{
return repositoryUrl;
}
public String getIndexUpdateUrl()
{
if ( repositoryUrl != null )
{
if ( indexUpdateUrl == null || indexUpdateUrl.trim().length() == 0 )
{
return repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + INDEX_DIRECTORY;
}
}
return indexUpdateUrl;
}
public Analyzer getAnalyzer()
{
return new NexusAnalyzer();
}
protected void openAndWarmup()
throws IOException
{
// IndexWriter (close)
if ( indexWriter != null )
{
indexWriter.close();
indexWriter = null;
}
if ( searcherManager != null )
{
searcherManager.close();
searcherManager = null;
}
this.indexWriter = new NexusIndexWriter( getIndexDirectory(), getWriterConfig() );
this.indexWriter.commit(); // LUCENE-2386
this.searcherManager = new SearcherManager( indexWriter, false, new NexusIndexSearcherFactory( this ) );
}
/**
* Returns new IndexWriterConfig instance
*
* @since 5.1
*/
protected IndexWriterConfig getWriterConfig()
{
return NexusIndexWriter.defaultConfig();
}
public IndexWriter getIndexWriter()
throws IOException
{
return indexWriter;
}
public IndexSearcher acquireIndexSearcher()
throws IOException
{
// TODO: move this to separate thread to not penalty next incoming searcher
searcherManager.maybeRefresh();
return searcherManager.acquire();
}
public void releaseIndexSearcher( final IndexSearcher is )
throws IOException
{
if ( is == null )
{
return;
}
searcherManager.release( is );
}
public void commit()
throws IOException
{
getIndexWriter().commit();
}
public void rollback()
throws IOException
{
getIndexWriter().rollback();
}
public synchronized void optimize()
throws CorruptIndexException, IOException
{
commit();
}
public synchronized void close( boolean deleteFiles )
throws IOException
{
if ( indexDirectory != null )
{
IndexUtils.updateTimestamp( indexDirectory, getTimestamp() );
closeReaders();
if ( deleteFiles )
{
deleteIndexFiles( true );
}
indexDirectory.close();
}
indexDirectory = null;
}
public synchronized void purge()
throws IOException
{
closeReaders();
deleteIndexFiles( true );
openAndWarmup();
try
{
prepareIndex( true );
}
catch ( ExistingLuceneIndexMismatchException e )
{
// just deleted it
}
rebuildGroups();
updateTimestamp( true, null );
}
public synchronized void replace( Directory directory )
throws IOException
{
replace( directory, null, null );
}
public synchronized void replace( Directory directory, Set<String> allGroups, Set<String> rootGroups )
throws IOException
{
final Date ts = IndexUtils.getTimestamp( directory );
closeReaders();
deleteIndexFiles( false );
IndexUtils.copyDirectory( directory, indexDirectory );
openAndWarmup();
// reclaim the index as mine
storeDescriptor();
if ( allGroups == null && rootGroups == null )
{
rebuildGroups();
}
else
{
if ( allGroups != null )
{
setAllGroups( allGroups );
}
if ( rootGroups != null )
{
setRootGroups( rootGroups );
}
}
updateTimestamp( true, ts );
optimize();
}
public synchronized void merge( Directory directory )
throws IOException
{
merge( directory, null );
}
public synchronized void merge( Directory directory, DocumentFilter filter )
throws IOException
{
final IndexSearcher s = acquireIndexSearcher();
try
{
final IndexWriter w = getIndexWriter();
final IndexReader directoryReader = DirectoryReader.open( directory );
TopScoreDocCollector collector = null;
try
{
int numDocs = directoryReader.maxDoc();
Bits liveDocs = MultiFields.getLiveDocs( directoryReader );
for ( int i = 0; i < numDocs; i++ )
{
if ( liveDocs != null && !liveDocs.get( i ) )
{
continue;
}
Document d = directoryReader.document( i );
if ( filter != null && !filter.accept( d ) )
{
continue;
}
String uinfo = d.get( ArtifactInfo.UINFO );
if ( uinfo != null )
{
collector = TopScoreDocCollector.create( 1 );
s.search( new TermQuery( new Term( ArtifactInfo.UINFO, uinfo ) ), collector );
if ( collector.getTotalHits() == 0 )
{
w.addDocument( IndexUtils.updateDocument( d, this, false ) );
}
}
else
{
String deleted = d.get( ArtifactInfo.DELETED );
if ( deleted != null )
{
// Deleting the document loses history that it was delete,
// so incrementals wont work. Therefore, put the delete
// document in as well
w.deleteDocuments( new Term( ArtifactInfo.UINFO, deleted ) );
w.addDocument( d );
}
}
}
}
finally
{
directoryReader.close();
commit();
}
rebuildGroups();
Date mergedTimestamp = IndexUtils.getTimestamp( directory );
if ( getTimestamp() != null && mergedTimestamp != null && mergedTimestamp.after( getTimestamp() ) )
{
// we have both, keep the newest
updateTimestamp( true, mergedTimestamp );
}
else
{
updateTimestamp( true );
}
optimize();
}
finally
{
releaseIndexSearcher( s );
}
}
private void closeReaders()
throws CorruptIndexException, IOException
{
if ( searcherManager != null )
{
searcherManager.close();
searcherManager = null;
}
if ( indexWriter != null )
{
indexWriter.close();
indexWriter = null;
}
}
public GavCalculator getGavCalculator()
{
return gavCalculator;
}
public List<IndexCreator> getIndexCreators()
{
return Collections.<IndexCreator>unmodifiableList( indexCreators );
}
// groups
public synchronized void rebuildGroups()
throws IOException
{
final IndexSearcher is = acquireIndexSearcher();
try
{
final IndexReader r = is.getIndexReader();
Set<String> rootGroups = new LinkedHashSet<String>();
Set<String> allGroups = new LinkedHashSet<String>();
int numDocs = r.maxDoc();
Bits liveDocs = MultiFields.getLiveDocs( r );
for ( int i = 0; i < numDocs; i++ )
{
if ( liveDocs != null && !liveDocs.get( i ) )
{
continue;
}
Document d = r.document( i );
String uinfo = d.get( ArtifactInfo.UINFO );
if ( uinfo != null )
{
ArtifactInfo info = IndexUtils.constructArtifactInfo( d, this );
rootGroups.add( info.getRootGroup() );
allGroups.add( info.getGroupId() );
}
}
setRootGroups( rootGroups );
setAllGroups( allGroups );
optimize();
}
finally
{
releaseIndexSearcher( is );
}
}
public Set<String> getAllGroups()
throws IOException
{
return getGroups( ArtifactInfo.ALL_GROUPS, ArtifactInfo.ALL_GROUPS_VALUE, ArtifactInfo.ALL_GROUPS_LIST );
}
public synchronized void setAllGroups( Collection<String> groups )
throws IOException
{
setGroups( groups, ArtifactInfo.ALL_GROUPS, ArtifactInfo.ALL_GROUPS_VALUE, ArtifactInfo.ALL_GROUPS_LIST );
commit();
}
public Set<String> getRootGroups()
throws IOException
{
return getGroups( ArtifactInfo.ROOT_GROUPS, ArtifactInfo.ROOT_GROUPS_VALUE, ArtifactInfo.ROOT_GROUPS_LIST );
}
public synchronized void setRootGroups( Collection<String> groups )
throws IOException
{
setGroups( groups, ArtifactInfo.ROOT_GROUPS, ArtifactInfo.ROOT_GROUPS_VALUE, ArtifactInfo.ROOT_GROUPS_LIST );
commit();
}
protected Set<String> getGroups( String field, String filedValue, String listField )
throws IOException, CorruptIndexException
{
final TopScoreDocCollector collector = TopScoreDocCollector.create( 1 );
final IndexSearcher indexSearcher = acquireIndexSearcher();
try
{
indexSearcher.search( new TermQuery( new Term( field, filedValue ) ), collector );
TopDocs topDocs = collector.topDocs();
Set<String> groups = new LinkedHashSet<String>( Math.max( 10, topDocs.totalHits ) );
if ( topDocs.totalHits > 0 )
{
Document doc = indexSearcher.doc( topDocs.scoreDocs[0].doc );
String groupList = doc.get( listField );
if ( groupList != null )
{
groups.addAll( Arrays.asList( groupList.split( "\\|" ) ) );
}
}
return groups;
}
finally
{
releaseIndexSearcher( indexSearcher );
}
}
protected void setGroups( Collection<String> groups, String groupField, String groupFieldValue,
String groupListField )
throws IOException, CorruptIndexException
{
final IndexWriter w = getIndexWriter();
w.updateDocument( new Term( groupField, groupFieldValue ),
createGroupsDocument( groups, groupField, groupFieldValue, groupListField ) );
}
protected Document createGroupsDocument( Collection<String> groups, String field, String fieldValue,
String listField )
{
final Document groupDoc = new Document();
groupDoc.add( new Field( field, fieldValue, IndexerField.KEYWORD_STORED ) );
groupDoc.add( new StoredField( listField, ArtifactInfo.lst2str( groups ) ) );
return groupDoc;
}
@Override
public String toString()
{
return id + " : " + timestamp;
}
private static void unlockForcibly( final TrackingLockFactory lockFactory, final Directory dir )
throws IOException
{
//Warning: Not doable in lucene >= 5.3 consider to remove it as IndexWriter.unlock
//was always strongly non recommended by Lucene.
//For now try to do the best to simulate the IndexWriter.unlock at least on FSDirectory
//using FSLockFactory, the RAMDirectory uses SingleInstanceLockFactory.
//custom lock factory?
if ( lockFactory != null )
{
final Set<? extends Lock> emittedLocks = lockFactory.getEmittedLocks( IndexWriter.WRITE_LOCK_NAME );
for ( Lock emittedLock : emittedLocks )
{
emittedLock.close();
}
}
if ( dir instanceof FSDirectory )
{
final FSDirectory fsdir = (FSDirectory) dir;
final Path dirPath = fsdir.getDirectory();
if ( Files.isDirectory( dirPath ) )
{
Path lockPath = dirPath.resolve( IndexWriter.WRITE_LOCK_NAME );
try
{
lockPath = lockPath.toRealPath();
}
catch ( IOException ioe )
{
// Not locked
return;
}
try ( final FileChannel fc =
FileChannel.open( lockPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE ) )
{
final FileLock lck = fc.tryLock();
if ( lck == null )
{
// Still active
throw new LockObtainFailedException( "Lock held by another process: " + lockPath );
}
else
{
// Not held fine to release
lck.close();
}
}
Files.delete( lockPath );
}
}
}
}