| package org.apache.maven.index.reader; |
| |
| /* |
| * 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.Closeable; |
| import java.io.IOException; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Properties; |
| |
| import org.apache.maven.index.reader.ResourceHandler.Resource; |
| |
| import static org.apache.maven.index.reader.Utils.loadProperties; |
| import static org.apache.maven.index.reader.Utils.storeProperties; |
| |
| /** |
| * Maven 2 Index reader that handles incremental updates if possible and provides one or more {@link ChunkReader}s, to |
| * read all the required records. |
| * |
| * @since 5.1.2 |
| */ |
| public class IndexReader |
| implements Iterable<ChunkReader>, Closeable |
| { |
| private final WritableResourceHandler local; |
| |
| private final ResourceHandler remote; |
| |
| private final Properties localIndexProperties; |
| |
| private final Properties remoteIndexProperties; |
| |
| private final String indexId; |
| |
| private final Date publishedTimestamp; |
| |
| private final boolean incremental; |
| |
| private final List<String> chunkNames; |
| |
| public IndexReader(final WritableResourceHandler local, final ResourceHandler remote) throws IOException { |
| if (remote == null) { |
| throw new NullPointerException("remote resource handler null"); |
| } |
| this.local = local; |
| this.remote = remote; |
| remoteIndexProperties = loadProperties(remote.locate(Utils.INDEX_FILE_PREFIX + ".properties")); |
| if (remoteIndexProperties == null) { |
| throw new IllegalArgumentException("Non-existent remote index"); |
| } |
| try { |
| if (local != null) { |
| Properties localProperties = loadProperties(local.locate(Utils.INDEX_FILE_PREFIX + ".properties")); |
| if (localProperties != null) { |
| this.localIndexProperties = localProperties; |
| String remoteIndexId = remoteIndexProperties.getProperty("nexus.index.id"); |
| String localIndexId = localIndexProperties.getProperty("nexus.index.id"); |
| if (remoteIndexId == null || localIndexId == null || !remoteIndexId.equals(localIndexId)) { |
| throw new IllegalArgumentException( |
| "local and remote index IDs does not match or is null: " + localIndexId + ", " + |
| remoteIndexId); |
| } |
| this.indexId = localIndexId; |
| this.incremental = canRetrieveAllChunks(); |
| } |
| else { |
| localIndexProperties = null; |
| this.indexId = remoteIndexProperties.getProperty("nexus.index.id"); |
| this.incremental = false; |
| } |
| } |
| else { |
| localIndexProperties = null; |
| this.indexId = remoteIndexProperties.getProperty("nexus.index.id"); |
| this.incremental = false; |
| } |
| this.publishedTimestamp = Utils.INDEX_DATE_FORMAT.parse(remoteIndexProperties.getProperty("nexus.index.timestamp")); |
| this.chunkNames = calculateChunkNames(); |
| } |
| catch (ParseException e) { |
| IOException ex = new IOException("Index properties corrupted"); |
| ex.initCause(e); |
| throw ex; |
| } |
| } |
| |
| /** |
| * Returns the index context ID that published index has set. Usually it is equal to "repository ID" used in {@link |
| * Record.Type#DESCRIPTOR} but does not have to be. |
| */ |
| public String getIndexId() { |
| return indexId; |
| } |
| |
| /** |
| * Returns the {@link Date} when remote index was last published. |
| */ |
| public Date getPublishedTimestamp() { |
| return publishedTimestamp; |
| } |
| |
| /** |
| * Returns {@code true} if incremental update is about to happen. If incremental update, the {@link #iterator()} will |
| * return only the diff from the last update. |
| */ |
| public boolean isIncremental() { |
| return incremental; |
| } |
| |
| /** |
| * Returns unmodifiable list of actual chunks that needs to be pulled from remote {@link ResourceHandler}. Those are |
| * incremental chunks or the big main file, depending on result of {@link #isIncremental()}. Empty list means local |
| * index is up to date, and {@link #iterator()} will return empty iterator. |
| */ |
| public List<String> getChunkNames() { |
| return chunkNames; |
| } |
| |
| /** |
| * Closes the underlying {@link ResourceHandler}s. In case of incremental update use, it also assumes that user |
| * consumed all the iterator and integrated it, hence, it will update the {@link WritableResourceHandler} contents to |
| * prepare it for future incremental update. If this is not desired (ie. due to aborted update), then this method |
| * should NOT be invoked, but rather the {@link ResourceHandler}s that caller provided in constructor of |
| * this class should be closed manually. |
| */ |
| public void close() throws IOException { |
| remote.close(); |
| if (local != null) { |
| try { |
| syncLocalWithRemote(); |
| } |
| finally { |
| local.close(); |
| } |
| } |
| } |
| |
| /** |
| * Returns an {@link Iterator} of {@link ChunkReader}s, that if read in sequence, provide all the (incremental) |
| * updates from the index. It is caller responsibility to either consume fully this iterator, or to close current |
| * {@link ChunkReader} if aborting. |
| */ |
| public Iterator<ChunkReader> iterator() { |
| return new ChunkReaderIterator(remote, chunkNames.iterator()); |
| } |
| |
| /** |
| * Stores the remote index properties into local index properties, preparing local {@link WritableResourceHandler} |
| * for future incremental updates. |
| */ |
| private void syncLocalWithRemote() throws IOException { |
| storeProperties(local.locate(Utils.INDEX_FILE_PREFIX + ".properties"), remoteIndexProperties); |
| } |
| |
| /** |
| * Calculates the chunk names that needs to be fetched. |
| */ |
| private List<String> calculateChunkNames() { |
| if (incremental) { |
| ArrayList<String> chunkNames = new ArrayList<>(); |
| int maxCounter = Integer.parseInt(remoteIndexProperties.getProperty("nexus.index.last-incremental")); |
| int currentCounter = Integer.parseInt(localIndexProperties.getProperty("nexus.index.last-incremental")); |
| currentCounter++; |
| while (currentCounter <= maxCounter) { |
| chunkNames.add(Utils.INDEX_FILE_PREFIX + "." + currentCounter++ + ".gz"); |
| } |
| return Collections.unmodifiableList(chunkNames); |
| } |
| else { |
| return Collections.singletonList(Utils.INDEX_FILE_PREFIX + ".gz"); |
| } |
| } |
| |
| /** |
| * Verifies incremental update is possible, as all the diff chunks we need are still enlisted in remote properties. |
| */ |
| private boolean canRetrieveAllChunks() |
| { |
| String localChainId = localIndexProperties.getProperty("nexus.index.chain-id"); |
| String remoteChainId = remoteIndexProperties.getProperty("nexus.index.chain-id"); |
| |
| // If no chain id, or not the same, do full update |
| if (localChainId == null || remoteChainId == null || !localChainId.equals(remoteChainId)) { |
| return false; |
| } |
| |
| try { |
| int localLastIncremental = Integer.parseInt(localIndexProperties.getProperty("nexus.index.last-incremental")); |
| String currentLocalCounter = String.valueOf(localLastIncremental); |
| String nextLocalCounter = String.valueOf(localLastIncremental + 1); |
| // check remote props for existence of current or next chunk after local |
| for (Object key : remoteIndexProperties.keySet()) { |
| String sKey = (String) key; |
| if (sKey.startsWith("nexus.index.incremental-")) { |
| String value = remoteIndexProperties.getProperty(sKey); |
| if (currentLocalCounter.equals(value) || nextLocalCounter.equals(value)) { |
| return true; |
| } |
| } |
| } |
| } |
| catch (NumberFormatException e) { |
| // fall through |
| } |
| return false; |
| } |
| |
| /** |
| * Internal iterator implementation that lazily opens and closes the returned {@link ChunkReader}s as this iterator |
| * is being consumed. |
| */ |
| private static class ChunkReaderIterator |
| implements Iterator<ChunkReader> |
| { |
| private final ResourceHandler resourceHandler; |
| |
| private final Iterator<String> chunkNamesIterator; |
| |
| private Resource currentResource; |
| |
| private ChunkReader currentChunkReader; |
| |
| private ChunkReaderIterator(final ResourceHandler resourceHandler, final Iterator<String> chunkNamesIterator) { |
| this.resourceHandler = resourceHandler; |
| this.chunkNamesIterator = chunkNamesIterator; |
| } |
| |
| public boolean hasNext() { |
| return chunkNamesIterator.hasNext(); |
| } |
| |
| public ChunkReader next() { |
| String chunkName = chunkNamesIterator.next(); |
| try { |
| if (currentChunkReader != null) { |
| currentChunkReader.close(); |
| } |
| currentResource = resourceHandler.locate(chunkName); |
| currentChunkReader = new ChunkReader(chunkName, currentResource.read()); |
| return currentChunkReader; |
| } |
| catch (IOException e) { |
| throw new RuntimeException("IO problem while switching chunk readers", e); |
| } |
| } |
| |
| public void remove() { |
| throw new UnsupportedOperationException("remove"); |
| } |
| } |
| } |