View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.index.incremental;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.text.ParseException;
27  import java.text.SimpleDateFormat;
28  import java.util.ArrayList;
29  import java.util.Date;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Map.Entry;
34  import java.util.Properties;
35  import java.util.Set;
36  import java.util.TimeZone;
37  import java.util.TreeMap;
38  
39  import org.apache.lucene.document.Document;
40  import org.apache.lucene.index.IndexReader;
41  import org.apache.lucene.index.MultiBits;
42  import org.apache.lucene.util.Bits;
43  import org.apache.maven.index.ArtifactInfo;
44  import org.apache.maven.index.context.IndexingContext;
45  import org.apache.maven.index.packer.IndexPackingRequest;
46  import org.apache.maven.index.updater.IndexUpdateRequest;
47  import org.codehaus.plexus.util.StringUtils;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  @Singleton
52  @Named
53  public class DefaultIncrementalHandler implements IncrementalHandler {
54  
55      private final Logger logger = LoggerFactory.getLogger(getClass());
56  
57      protected Logger getLogger() {
58          return logger;
59      }
60  
61      public List<Integer> getIncrementalUpdates(IndexPackingRequest request, Properties properties) throws IOException {
62          getLogger().debug("Handling Incremental Updates");
63  
64          if (!validateProperties(properties)) {
65              getLogger().debug("Invalid properties found, resetting them and doing no incremental packing.");
66              return null;
67          }
68  
69          // Get the list of document ids that have been added since the last time
70          // the index ran
71          List<Integer> chunk = getIndexChunk(request, parse(properties.getProperty(IndexingContext.INDEX_TIMESTAMP)));
72  
73          getLogger().debug("Found " + chunk.size() + " differences to put in incremental index.");
74  
75          // if no documents, then we don't need to do anything, no changes
76          if (chunk.size() > 0) {
77              updateProperties(properties, request);
78          }
79  
80          cleanUpIncrementalChunks(request, properties);
81  
82          return chunk;
83      }
84  
85      public List<String> loadRemoteIncrementalUpdates(
86              IndexUpdateRequest request, Properties localProperties, Properties remoteProperties) throws IOException {
87          List<String> filenames = null;
88          // If we have local properties, will parse and see what we need to download
89          if (canRetrieveAllChunks(localProperties, remoteProperties)) {
90              filenames = new ArrayList<>();
91  
92              int maxCounter = Integer.parseInt(remoteProperties.getProperty(IndexingContext.INDEX_CHUNK_COUNTER));
93              int currentCounter = Integer.parseInt(localProperties.getProperty(IndexingContext.INDEX_CHUNK_COUNTER));
94  
95              // Start with the next one
96              currentCounter++;
97  
98              while (currentCounter <= maxCounter) {
99                  filenames.add(IndexingContext.INDEX_FILE_PREFIX + "." + currentCounter++ + ".gz");
100             }
101         }
102 
103         return filenames;
104     }
105 
106     private boolean validateProperties(Properties properties) {
107         if (properties == null || properties.isEmpty()) {
108             return false;
109         }
110 
111         if (properties.getProperty(IndexingContext.INDEX_TIMESTAMP) == null) {
112             return false;
113         }
114 
115         if (parse(properties.getProperty(IndexingContext.INDEX_TIMESTAMP)) == null) {
116             return false;
117         }
118 
119         initializeProperties(properties);
120 
121         return true;
122     }
123 
124     public void initializeProperties(Properties properties) {
125         if (properties.getProperty(IndexingContext.INDEX_CHAIN_ID) == null) {
126             properties.setProperty(IndexingContext.INDEX_CHAIN_ID, Long.toString(new Date().getTime()));
127             properties.remove(IndexingContext.INDEX_CHUNK_COUNTER);
128         }
129 
130         if (properties.getProperty(IndexingContext.INDEX_CHUNK_COUNTER) == null) {
131             properties.setProperty(IndexingContext.INDEX_CHUNK_COUNTER, "0");
132         }
133     }
134 
135     private List<Integer> getIndexChunk(IndexPackingRequest request, Date timestamp) throws IOException {
136         final List<Integer> chunk = new ArrayList<>();
137         final IndexReader r = request.getIndexReader();
138         Bits liveDocs = MultiBits.getLiveDocs(r);
139         for (int i = 0; i < r.maxDoc(); i++) {
140             if (liveDocs == null || liveDocs.get(i)) {
141                 Document d = r.document(i);
142 
143                 String lastModified = d.get(ArtifactInfo.LAST_MODIFIED);
144 
145                 if (lastModified != null) {
146                     Date t = new Date(Long.parseLong(lastModified));
147 
148                     // Only add documents that were added after the last time we indexed
149                     if (t.after(timestamp)) {
150                         chunk.add(i);
151                     }
152                 }
153             }
154         }
155 
156         return chunk;
157     }
158 
159     private void updateProperties(Properties properties, IndexPackingRequest request) throws IOException {
160         Set<Object> keys = new HashSet<>(properties.keySet());
161         Map<Integer, String> dataMap = new TreeMap<>();
162 
163         // First go through and retrieve all keys and their values
164         for (Object key : keys) {
165             String sKey = (String) key;
166 
167             if (sKey.startsWith(IndexingContext.INDEX_CHUNK_PREFIX)) {
168                 Integer count = Integer.valueOf(sKey.substring(IndexingContext.INDEX_CHUNK_PREFIX.length()));
169                 String value = properties.getProperty(sKey);
170 
171                 dataMap.put(count, value);
172                 properties.remove(key);
173             }
174         }
175 
176         String val = properties.getProperty(IndexingContext.INDEX_CHUNK_COUNTER);
177 
178         int i = 0;
179         // Next put the items back in w/ proper keys
180         for (Entry<Integer, String> entry : dataMap.entrySet()) {
181             // make sure to end if we reach limit, 0 based
182             if (i >= (request.getMaxIndexChunks() - 1)) {
183                 break;
184             }
185 
186             properties.put(IndexingContext.INDEX_CHUNK_PREFIX + (entry.getKey() + 1), entry.getValue());
187 
188             i++;
189         }
190 
191         int nextValue = Integer.parseInt(val) + 1;
192 
193         // Now put the new one in, and update the counter
194         properties.put(IndexingContext.INDEX_CHUNK_PREFIX + "0", Integer.toString(nextValue));
195         properties.put(IndexingContext.INDEX_CHUNK_COUNTER, Integer.toString(nextValue));
196     }
197 
198     private void cleanUpIncrementalChunks(IndexPackingRequest request, Properties properties) {
199         File[] files = request.getTargetDir().listFiles((dir, name) -> {
200             String[] parts = name.split("\\.");
201             return parts.length == 3 && parts[0].equals(IndexingContext.INDEX_FILE_PREFIX) && parts[2].equals("gz");
202         });
203 
204         for (File file : files) {
205             String[] parts = file.getName().split("\\.");
206 
207             boolean found = false;
208             for (Entry<Object, Object> entry : properties.entrySet()) {
209                 if (entry.getKey().toString().startsWith(IndexingContext.INDEX_CHUNK_PREFIX)
210                         && entry.getValue().equals(parts[1])) {
211                     found = true;
212                     break;
213                 }
214             }
215 
216             if (!found) {
217                 file.delete();
218             }
219         }
220     }
221 
222     private Date parse(String s) {
223         try {
224             SimpleDateFormat df = new SimpleDateFormat(IndexingContext.INDEX_TIME_FORMAT);
225             df.setTimeZone(TimeZone.getTimeZone("GMT"));
226             return df.parse(s);
227         } catch (ParseException e) {
228             return null;
229         }
230     }
231 
232     private boolean canRetrieveAllChunks(Properties localProps, Properties remoteProps) {
233         // no localprops, can't retrieve chunks
234         if (localProps == null) {
235             return false;
236         }
237 
238         String localChainId = localProps.getProperty(IndexingContext.INDEX_CHAIN_ID);
239         String remoteChainId = remoteProps.getProperty(IndexingContext.INDEX_CHAIN_ID);
240 
241         // If no chain id, or not the same, do whole download
242         if (StringUtils.isEmpty(localChainId) || !localChainId.equals(remoteChainId)) {
243             return false;
244         }
245 
246         String counterProp = localProps.getProperty(IndexingContext.INDEX_CHUNK_COUNTER);
247 
248         // no counter, cant retrieve chunks
249         // not a number, cant retrieve chunks
250         if (StringUtils.isEmpty(counterProp) || !StringUtils.isNumeric(counterProp)) {
251             return false;
252         }
253 
254         int currentLocalCounter = Integer.parseInt(counterProp);
255 
256         // check remote props for existence of next chunk after local
257         // if we find it, then we are ok to retrieve the rest of the chunks
258         for (Object key : remoteProps.keySet()) {
259             String sKey = (String) key;
260 
261             if (sKey.startsWith(IndexingContext.INDEX_CHUNK_PREFIX)) {
262                 String value = remoteProps.getProperty(sKey);
263 
264                 // If we have the current counter, or the next counter, we are good to go
265                 if (Integer.toString(currentLocalCounter).equals(value)
266                         || Integer.toString(currentLocalCounter + 1).equals(value)) {
267                     return true;
268                 }
269             }
270         }
271 
272         return false;
273     }
274 }