View Javadoc
1   package org.apache.maven.index.reader;
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 org.apache.maven.index.reader.ResourceHandler.Resource;
23  
24  import java.io.Closeable;
25  import java.io.IOException;
26  import java.text.ParseException;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Date;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Objects;
33  import java.util.Properties;
34  
35  import static org.apache.maven.index.reader.Utils.loadProperties;
36  import static org.apache.maven.index.reader.Utils.storeProperties;
37  
38  /**
39   * Maven 2 Index reader that handles incremental updates if possible and provides one or more {@link ChunkReader}s, to
40   * read all the required records.
41   *
42   * @since 5.1.2
43   */
44  public class IndexReader
45      implements Iterable<ChunkReader>, Closeable
46  {
47      private final WritableResourceHandler local;
48  
49      private final ResourceHandler remote;
50  
51      private final Properties localIndexProperties;
52  
53      private final Properties remoteIndexProperties;
54  
55      private final String indexId;
56  
57      private final Date publishedTimestamp;
58  
59      private final boolean incremental;
60  
61      private final List<String> chunkNames;
62  
63      public IndexReader( final WritableResourceHandler local, final ResourceHandler remote )
64          throws IOException
65      {
66          Objects.requireNonNull( remote, "remote resource handler null" );
67          this.local = local;
68          this.remote = remote;
69          remoteIndexProperties = loadProperties( remote.locate( Utils.INDEX_FILE_PREFIX + ".properties" ) );
70          if ( remoteIndexProperties == null )
71          {
72              throw new IllegalArgumentException( "Non-existent remote index" );
73          }
74          try
75          {
76              if ( local != null )
77              {
78                  Properties localProperties =
79                          loadProperties( local.locate( Utils.INDEX_FILE_PREFIX + ".properties" ) );
80                  if ( localProperties != null )
81                  {
82                      this.localIndexProperties = localProperties;
83                      String remoteIndexId = remoteIndexProperties.getProperty( "nexus.index.id" );
84                      String localIndexId = localIndexProperties.getProperty( "nexus.index.id" );
85                      if ( remoteIndexId == null || !remoteIndexId.equals( localIndexId ) )
86                      {
87                          throw new IllegalArgumentException(
88                              "local and remote index IDs does not match or is null: " + localIndexId + ", "
89                                  + remoteIndexId );
90                      }
91                      this.indexId = localIndexId;
92                      this.incremental = canRetrieveAllChunks();
93                  }
94                  else
95                  {
96                      localIndexProperties = null;
97                      this.indexId = remoteIndexProperties.getProperty( "nexus.index.id" );
98                      this.incremental = false;
99                  }
100             }
101             else
102             {
103                 localIndexProperties = null;
104                 this.indexId = remoteIndexProperties.getProperty( "nexus.index.id" );
105                 this.incremental = false;
106             }
107             this.publishedTimestamp =
108                 Utils.INDEX_DATE_FORMAT.parse( remoteIndexProperties.getProperty( "nexus.index.timestamp" ) );
109             this.chunkNames = calculateChunkNames();
110         }
111         catch ( ParseException e )
112         {
113             throw new IOException( "Index properties corrupted", e );
114         }
115     }
116 
117     /**
118      * Returns the index context ID that published index has set. Usually it is equal to "repository ID" used in {@link
119      * Record.Type#DESCRIPTOR} but does not have to be.
120      */
121     public String getIndexId()
122     {
123         return indexId;
124     }
125 
126     /**
127      * Returns the {@link Date} when remote index was last published.
128      */
129     public Date getPublishedTimestamp()
130     {
131         return publishedTimestamp;
132     }
133 
134     /**
135      * Returns {@code true} if incremental update is about to happen. If incremental update, the {@link #iterator()}
136      * will return only the diff from the last update.
137      */
138     public boolean isIncremental()
139     {
140         return incremental;
141     }
142 
143     /**
144      * Returns unmodifiable list of actual chunks that needs to be pulled from remote {@link ResourceHandler}. Those are
145      * incremental chunks or the big main file, depending on result of {@link #isIncremental()}. Empty list means local
146      * index is up to date, and {@link #iterator()} will return empty iterator.
147      */
148     public List<String> getChunkNames()
149     {
150         return chunkNames;
151     }
152 
153     /**
154      * Closes the underlying {@link ResourceHandler}s. In case of incremental update use, it also assumes that user
155      * consumed all the iterator and integrated it, hence, it will update the {@link WritableResourceHandler} contents
156      * to prepare it for future incremental update. If this is not desired (ie. due to aborted update), then this
157      * method should NOT be invoked, but rather the {@link ResourceHandler}s that caller provided in constructor of
158      * this class should be closed manually.
159      */
160     public void close()
161         throws IOException
162     {
163         remote.close();
164         if ( local != null )
165         {
166             try
167             {
168                 syncLocalWithRemote();
169             }
170             finally
171             {
172                 local.close();
173             }
174         }
175     }
176 
177     /**
178      * Returns an {@link Iterator} of {@link ChunkReader}s, that if read in sequence, provide all the (incremental)
179      * updates from the index. It is caller responsibility to either consume fully this iterator, or to close current
180      * {@link ChunkReader} if aborting.
181      */
182     public Iterator<ChunkReader> iterator()
183     {
184         return new ChunkReaderIterator( remote, chunkNames.iterator() );
185     }
186 
187     /**
188      * Stores the remote index properties into local index properties, preparing local {@link WritableResourceHandler}
189      * for future incremental updates.
190      */
191     private void syncLocalWithRemote()
192         throws IOException
193     {
194         storeProperties( local.locate( Utils.INDEX_FILE_PREFIX + ".properties" ), remoteIndexProperties );
195     }
196 
197     /**
198      * Calculates the chunk names that needs to be fetched.
199      */
200     private List<String> calculateChunkNames()
201     {
202         if ( incremental )
203         {
204             ArrayList<String> chunkNames = new ArrayList<>();
205             int maxCounter = Integer.parseInt( remoteIndexProperties.getProperty( "nexus.index.last-incremental" ) );
206             int currentCounter = Integer.parseInt( localIndexProperties.getProperty( "nexus.index.last-incremental" ) );
207             currentCounter++;
208             while ( currentCounter <= maxCounter )
209             {
210                 chunkNames.add( Utils.INDEX_FILE_PREFIX + "." + currentCounter++ + ".gz" );
211             }
212             return Collections.unmodifiableList( chunkNames );
213         }
214         else
215         {
216             return Collections.singletonList( Utils.INDEX_FILE_PREFIX + ".gz" );
217         }
218     }
219 
220     /**
221      * Verifies incremental update is possible, as all the diff chunks we need are still enlisted in remote properties.
222      */
223     private boolean canRetrieveAllChunks()
224     {
225         String localChainId = localIndexProperties.getProperty( "nexus.index.chain-id" );
226         String remoteChainId = remoteIndexProperties.getProperty( "nexus.index.chain-id" );
227 
228         // If no chain id, or not the same, do full update
229         if ( localChainId == null || !localChainId.equals( remoteChainId ) )
230         {
231             return false;
232         }
233 
234         try
235         {
236             int localLastIncremental =
237                 Integer.parseInt( localIndexProperties.getProperty( "nexus.index.last-incremental" ) );
238             String currentLocalCounter = String.valueOf( localLastIncremental );
239             String nextLocalCounter = String.valueOf( localLastIncremental + 1 );
240             // check remote props for existence of current or next chunk after local
241             for ( Object key : remoteIndexProperties.keySet() )
242             {
243                 String sKey = (String) key;
244                 if ( sKey.startsWith( "nexus.index.incremental-" ) )
245                 {
246                     String value = remoteIndexProperties.getProperty( sKey );
247                     if ( currentLocalCounter.equals( value ) || nextLocalCounter.equals( value ) )
248                     {
249                         return true;
250                     }
251                 }
252             }
253         }
254         catch ( NumberFormatException e )
255         {
256             // fall through
257         }
258         return false;
259     }
260 
261     /**
262      * Internal iterator implementation that lazily opens and closes the returned {@link ChunkReader}s as this iterator
263      * is being consumed.
264      */
265     private static class ChunkReaderIterator
266         implements Iterator<ChunkReader>
267     {
268         private final ResourceHandler resourceHandler;
269 
270         private final Iterator<String> chunkNamesIterator;
271 
272         private ChunkReader currentChunkReader;
273 
274         private ChunkReaderIterator( final ResourceHandler resourceHandler, final Iterator<String> chunkNamesIterator )
275         {
276             this.resourceHandler = resourceHandler;
277             this.chunkNamesIterator = chunkNamesIterator;
278             this.currentChunkReader = null;
279         }
280 
281         public boolean hasNext()
282         {
283             try
284             {
285                 if ( currentChunkReader != null )
286                 {
287                     currentChunkReader.close();
288                 }
289             }
290             catch ( IOException e )
291             {
292                 throw new RuntimeException( "IO problem while closing chunk readers", e );
293             }
294             return chunkNamesIterator.hasNext();
295         }
296 
297         public ChunkReader next()
298         {
299             String chunkName = chunkNamesIterator.next();
300             try
301             {
302                 Resource currentResource = resourceHandler.locate( chunkName );
303                 currentChunkReader = new ChunkReader( chunkName, currentResource.read() );
304                 return currentChunkReader;
305             }
306             catch ( IOException e )
307             {
308                 throw new RuntimeException( "IO problem while opening chunk readers", e );
309             }
310         }
311 
312         public void remove()
313         {
314             throw new UnsupportedOperationException( "remove" );
315         }
316     }
317 }