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