blob: d6c6e389bcf5a588413e733e789613e777c38bb6 [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.index.updater;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import org.apache.lucene.document.Document;
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.MultiBits;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Bits;
import org.apache.maven.index.context.DocumentFilter;
import org.apache.maven.index.context.IndexUtils;
import org.apache.maven.index.context.IndexingContext;
import org.apache.maven.index.context.NexusAnalyzer;
import org.apache.maven.index.context.NexusIndexWriter;
import org.apache.maven.index.fs.Lock;
import org.apache.maven.index.fs.Locker;
import org.apache.maven.index.incremental.IncrementalHandler;
import org.apache.maven.index.updater.IndexDataReader.IndexDataReadResult;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.io.RawInputStreamFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A default index updater implementation
*
* @author Jason van Zyl
* @author Eugene Kuleshov
*/
@Singleton
@Named
public class DefaultIndexUpdater implements IndexUpdater {
private final Logger logger = LoggerFactory.getLogger(getClass());
protected Logger getLogger() {
return logger;
}
private final IncrementalHandler incrementalHandler;
private final List<IndexUpdateSideEffect> sideEffects;
@Inject
public DefaultIndexUpdater(
final IncrementalHandler incrementalHandler, final List<IndexUpdateSideEffect> sideEffects) {
this.incrementalHandler = incrementalHandler;
this.sideEffects = sideEffects;
}
public IndexUpdateResult fetchAndUpdateIndex(final IndexUpdateRequest updateRequest) throws IOException {
IndexUpdateResult result = new IndexUpdateResult();
IndexingContext context = updateRequest.getIndexingContext();
ResourceFetcher fetcher = null;
if (!updateRequest.isOffline()) {
fetcher = updateRequest.getResourceFetcher();
// If no resource fetcher passed in, use the wagon fetcher by default
// and put back in request for future use
if (fetcher == null) {
throw new IOException("Update of the index without provided ResourceFetcher is impossible.");
}
fetcher.connect(context.getId(), context.getIndexUpdateUrl());
}
File cacheDir = updateRequest.getLocalIndexCacheDir();
Locker locker = updateRequest.getLocker();
Lock lock = locker != null && cacheDir != null ? locker.lock(cacheDir) : null;
try {
if (cacheDir != null) {
LocalCacheIndexAdaptor cache = new LocalCacheIndexAdaptor(cacheDir, result);
if (!updateRequest.isOffline()) {
cacheDir.mkdirs();
try {
if (fetchAndUpdateIndex(updateRequest, fetcher, cache).isSuccessful()) {
cache.commit();
}
} finally {
fetcher.disconnect();
}
}
fetcher = cache.getFetcher();
} else if (updateRequest.isOffline()) {
throw new IllegalArgumentException("LocalIndexCacheDir can not be null in offline mode");
}
try {
if (!updateRequest.isCacheOnly()) {
LuceneIndexAdaptor target = new LuceneIndexAdaptor(updateRequest);
result = fetchAndUpdateIndex(updateRequest, fetcher, target);
if (result.isSuccessful()) {
target.commit();
}
}
} finally {
fetcher.disconnect();
}
} finally {
if (lock != null) {
lock.release();
}
}
return result;
}
private Date loadIndexDirectory(
final IndexUpdateRequest updateRequest,
final ResourceFetcher fetcher,
final boolean merge,
final String remoteIndexFile)
throws IOException {
File indexDir;
if (updateRequest.getIndexTempDir() != null) {
updateRequest.getIndexTempDir().mkdirs();
indexDir = Files.createTempDirectory(updateRequest.getIndexTempDir().toPath(), remoteIndexFile + ".dir")
.toFile();
} else {
indexDir = Files.createTempDirectory(remoteIndexFile + ".dir").toFile();
}
try (BufferedInputStream is = new BufferedInputStream(fetcher.retrieve(remoteIndexFile)); //
Directory directory = updateRequest.getFSDirectoryFactory().open(indexDir)) {
Date timestamp;
Set<String> rootGroups;
Set<String> allGroups;
if (remoteIndexFile.endsWith(".gz")) {
IndexDataReadResult result =
unpackIndexData(is, updateRequest, directory, updateRequest.getIndexingContext());
timestamp = result.getTimestamp();
rootGroups = result.getRootGroups();
allGroups = result.getAllGroups();
} else {
// legacy transfer format
throw new IllegalArgumentException(
"The legacy format is no longer supported " + "by this version of maven-indexer.");
}
if (updateRequest.getDocumentFilter() != null) {
filterDirectory(directory, updateRequest.getDocumentFilter());
}
if (merge) {
updateRequest.getIndexingContext().merge(directory);
} else {
updateRequest.getIndexingContext().replace(directory, allGroups, rootGroups);
}
if (sideEffects != null && sideEffects.size() > 0) {
getLogger().info(IndexUpdateSideEffect.class.getName() + " extensions found: " + sideEffects.size());
for (IndexUpdateSideEffect sideeffect : sideEffects) {
sideeffect.updateIndex(directory, updateRequest.getIndexingContext(), merge);
}
}
return timestamp;
} finally {
try {
FileUtils.deleteDirectory(indexDir);
} catch (IOException ex) {
// ignore
}
}
}
private static void filterDirectory(final Directory directory, final DocumentFilter filter) throws IOException {
IndexReader r = null;
IndexWriter w = null;
try {
r = DirectoryReader.open(directory);
w = new NexusIndexWriter(directory, new IndexWriterConfig(new NexusAnalyzer()));
Bits liveDocs = MultiBits.getLiveDocs(r);
int numDocs = r.maxDoc();
for (int i = 0; i < numDocs; i++) {
if (liveDocs != null && !liveDocs.get(i)) {
continue;
}
Document d = r.storedFields().document(i);
if (!filter.accept(d)) {
boolean success = w.tryDeleteDocument(r, i) != -1;
// FIXME handle deletion failure
}
}
w.commit();
} finally {
IndexUtils.close(r);
IndexUtils.close(w);
}
w = null;
try {
// analyzer is unimportant, since we are not adding/searching to/on index, only reading/deleting
w = new NexusIndexWriter(directory, new IndexWriterConfig(new NexusAnalyzer()));
w.commit();
} finally {
IndexUtils.close(w);
}
}
private Properties loadIndexProperties(final File indexDirectoryFile, final String remoteIndexPropertiesName) {
File indexProperties = new File(indexDirectoryFile, remoteIndexPropertiesName);
try (FileInputStream fis = new FileInputStream(indexProperties)) {
Properties properties = new Properties();
properties.load(fis);
return properties;
} catch (IOException e) {
getLogger().debug("Unable to read remote properties stored locally", e);
}
return null;
}
private void storeIndexProperties(final File dir, final String indexPropertiesName, final Properties properties)
throws IOException {
File file = new File(dir, indexPropertiesName);
if (properties != null) {
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
properties.store(os, null);
}
} else {
file.delete();
}
}
private Properties downloadIndexProperties(final ResourceFetcher fetcher) throws IOException {
try (InputStream fis = fetcher.retrieve(IndexingContext.INDEX_REMOTE_PROPERTIES_FILE)) {
Properties properties = new Properties();
properties.load(fis);
return properties;
}
}
public Date getTimestamp(final Properties properties, final String key) {
String indexTimestamp = properties.getProperty(key);
if (indexTimestamp != null) {
try {
SimpleDateFormat df = new SimpleDateFormat(IndexingContext.INDEX_TIME_FORMAT);
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df.parse(indexTimestamp);
} catch (ParseException ex) {
}
}
return null;
}
/**
* @param is an input stream to unpack index data from
* @param threads thread count to use
* @param d
* @param context
*/
public static IndexDataReadResult unpackIndexData(
final InputStream is, final int threads, final Directory d, final IndexingContext context)
throws IOException {
return unpackIndexData(d, new IndexDataReader(is, threads), context);
}
/**
* @param is an input stream to unpack index data from
* @param request IndexUpdateRequest for configuration
* @param d
* @param context
*/
public static IndexDataReadResult unpackIndexData(
final InputStream is, final IndexUpdateRequest request, final Directory d, final IndexingContext context)
throws IOException {
return unpackIndexData(d, new IndexDataReader(is, request), context);
}
private static IndexDataReadResult unpackIndexData(
final Directory d, IndexDataReader dr, final IndexingContext context) throws IOException {
IndexWriterConfig config = new IndexWriterConfig(new NexusAnalyzer());
config.setUseCompoundFile(false);
try (NexusIndexWriter w = new NexusIndexWriter(d, config)) {
return dr.readIndex(w, context);
}
}
/**
* Filesystem-based ResourceFetcher implementation
*/
public static class FileFetcher implements ResourceFetcher {
private final File basedir;
public FileFetcher(File basedir) {
this.basedir = basedir;
}
public void connect(String id, String url) throws IOException {
// don't need to do anything
}
public void disconnect() throws IOException {
// don't need to do anything
}
public void retrieve(String name, File targetFile) throws IOException, FileNotFoundException {
FileUtils.copyFile(getFile(name), targetFile);
}
public InputStream retrieve(String name) throws IOException, FileNotFoundException {
return new FileInputStream(getFile(name));
}
private File getFile(String name) {
return new File(basedir, name);
}
}
private abstract class IndexAdaptor {
protected final File dir;
protected Properties properties;
protected IndexAdaptor(File dir) {
this.dir = dir;
}
public abstract Properties getProperties();
public abstract void storeProperties() throws IOException;
public abstract void addIndexChunk(ResourceFetcher source, String filename) throws IOException;
public abstract Date setIndexFile(ResourceFetcher source, String string) throws IOException;
public Properties setProperties(ResourceFetcher source) throws IOException {
this.properties = downloadIndexProperties(source);
return properties;
}
public abstract Date getTimestamp();
public void commit() throws IOException {
storeProperties();
}
}
private class LuceneIndexAdaptor extends IndexAdaptor {
private final IndexUpdateRequest updateRequest;
LuceneIndexAdaptor(IndexUpdateRequest updateRequest) {
super(updateRequest.getIndexingContext().getIndexDirectoryFile());
this.updateRequest = updateRequest;
}
public Properties getProperties() {
if (properties == null) {
properties = loadIndexProperties(dir, IndexingContext.INDEX_UPDATER_PROPERTIES_FILE);
}
return properties;
}
public void storeProperties() throws IOException {
storeIndexProperties(dir, IndexingContext.INDEX_UPDATER_PROPERTIES_FILE, properties);
}
public Date getTimestamp() {
return updateRequest.getIndexingContext().getTimestamp();
}
public void addIndexChunk(ResourceFetcher source, String filename) throws IOException {
loadIndexDirectory(updateRequest, source, true, filename);
}
public Date setIndexFile(ResourceFetcher source, String filename) throws IOException {
return loadIndexDirectory(updateRequest, source, false, filename);
}
public void commit() throws IOException {
super.commit();
updateRequest.getIndexingContext().commit();
}
}
private class LocalCacheIndexAdaptor extends IndexAdaptor {
private static final String CHUNKS_FILENAME = "chunks.lst";
private final IndexUpdateResult result;
private final ArrayList<String> newChunks = new ArrayList<>();
LocalCacheIndexAdaptor(File dir, IndexUpdateResult result) {
super(dir);
this.result = result;
}
public Properties getProperties() {
if (properties == null) {
properties = loadIndexProperties(dir, IndexingContext.INDEX_REMOTE_PROPERTIES_FILE);
}
return properties;
}
public void storeProperties() throws IOException {
storeIndexProperties(dir, IndexingContext.INDEX_REMOTE_PROPERTIES_FILE, properties);
}
public Date getTimestamp() {
Properties properties = getProperties();
if (properties == null) {
return null;
}
Date timestamp = DefaultIndexUpdater.this.getTimestamp(properties, IndexingContext.INDEX_TIMESTAMP);
if (timestamp == null) {
timestamp = DefaultIndexUpdater.this.getTimestamp(properties, IndexingContext.INDEX_LEGACY_TIMESTAMP);
}
return timestamp;
}
public void addIndexChunk(ResourceFetcher source, String filename) throws IOException {
File chunk = new File(dir, filename);
FileUtils.copyStreamToFile(new RawInputStreamFacade(source.retrieve(filename)), chunk);
newChunks.add(filename);
}
public Date setIndexFile(ResourceFetcher source, String filename) throws IOException {
cleanCacheDirectory(dir);
result.setFullUpdate(true);
File target = new File(dir, filename);
FileUtils.copyStreamToFile(new RawInputStreamFacade(source.retrieve(filename)), target);
return null;
}
@Override
public void commit() throws IOException {
File chunksFile = new File(dir, CHUNKS_FILENAME);
try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(chunksFile, true)); //
Writer w = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
for (String filename : newChunks) {
w.write(filename + "\n");
}
w.flush();
}
super.commit();
}
public List<String> getChunks() throws IOException {
ArrayList<String> chunks = new ArrayList<>();
File chunksFile = new File(dir, CHUNKS_FILENAME);
try (BufferedReader r = new BufferedReader(
new InputStreamReader(new FileInputStream(chunksFile), StandardCharsets.UTF_8))) {
String str;
while ((str = r.readLine()) != null) {
chunks.add(str);
}
}
return chunks;
}
public ResourceFetcher getFetcher() {
return new LocalIndexCacheFetcher(dir) {
@Override
public List<String> getChunks() throws IOException {
return LocalCacheIndexAdaptor.this.getChunks();
}
};
}
}
abstract static class LocalIndexCacheFetcher extends FileFetcher {
LocalIndexCacheFetcher(File basedir) {
super(basedir);
}
public abstract List<String> getChunks() throws IOException;
}
private IndexUpdateResult fetchAndUpdateIndex(
final IndexUpdateRequest updateRequest, ResourceFetcher source, IndexAdaptor target) throws IOException {
IndexUpdateResult result = new IndexUpdateResult();
if (!updateRequest.isForceFullUpdate()) {
Properties localProperties = target.getProperties();
Date localTimestamp = null;
if (localProperties != null) {
localTimestamp = getTimestamp(localProperties, IndexingContext.INDEX_TIMESTAMP);
}
// this will download and store properties in the target, so next run
// target.getProperties() will retrieve it
Properties remoteProperties = target.setProperties(source);
Date updateTimestamp = getTimestamp(remoteProperties, IndexingContext.INDEX_TIMESTAMP);
// If new timestamp is missing, dont bother checking incremental, we have an old file
if (updateTimestamp != null) {
List<String> filenames = incrementalHandler.loadRemoteIncrementalUpdates(
updateRequest, localProperties, remoteProperties);
// if we have some incremental files, merge them in
if (filenames != null) {
for (String filename : filenames) {
target.addIndexChunk(source, filename);
}
result.setTimestamp(updateTimestamp);
result.setSuccessful(true);
return result;
}
} else {
updateTimestamp = getTimestamp(remoteProperties, IndexingContext.INDEX_LEGACY_TIMESTAMP);
}
// fallback to timestamp comparison, but try with one coming from local properties, and if not possible (is
// null)
// fallback to context timestamp
if (localTimestamp != null) {
// if we have localTimestamp
// if incremental can't be done for whatever reason, simply use old logic of
// checking the timestamp, if the same, nothing to do
if (updateTimestamp != null && localTimestamp != null && !updateTimestamp.after(localTimestamp)) {
// Index is up to date
result.setSuccessful(true);
return result;
}
}
} else {
// create index properties during forced full index download
target.setProperties(source);
}
if (!updateRequest.isIncrementalOnly()) {
Date timestamp;
try {
timestamp = target.setIndexFile(source, IndexingContext.INDEX_FILE_PREFIX + ".gz");
if (source instanceof LocalIndexCacheFetcher) {
// local cache has inverse organization compared to remote indexes,
// i.e. initial index file and delta chunks to apply on top of it
for (String filename : ((LocalIndexCacheFetcher) source).getChunks()) {
target.addIndexChunk(source, filename);
}
}
} catch (IOException ex) {
// try to look for legacy index transfer format
try {
timestamp = target.setIndexFile(source, IndexingContext.INDEX_FILE_PREFIX + ".zip");
} catch (IOException ex2) {
getLogger().error("Fallback to *.zip also failed: " + ex2); // do not bother with stack trace
throw ex; // original exception more likely to be interesting
}
}
result.setTimestamp(timestamp);
result.setSuccessful(true);
result.setFullUpdate(true);
}
return result;
}
/**
* Cleans specified cache directory. If present, Locker.LOCK_FILE will not be deleted.
*/
protected void cleanCacheDirectory(File dir) throws IOException {
File[] members = dir.listFiles();
if (members == null) {
return;
}
for (File member : members) {
if (!Locker.LOCK_FILE.equals(member.getName())) {
FileUtils.forceDelete(member);
}
}
}
}