1   package com.trendmicro.grid.acl.ds.jpa;
2   
3   import com.trendmicro.grid.acl.WellKnownTags;
4   import com.trendmicro.grid.acl.ds.jpa.entities.*;
5   import com.trendmicro.grid.acl.l0.datatypes.*;
6   import org.slf4j.Logger;
7   import org.slf4j.LoggerFactory;
8   
9   import javax.persistence.Query;
10  import java.util.*;
11  
12  import static com.trendmicro.grid.acl.ds.jpa.ReceivedPreparedPackagesHandler.PreparedPackage;
13  import static com.trendmicro.grid.acl.ds.jpa.StorageOptions.PATH_DELIMITERS;
14  import static com.trendmicro.grid.acl.ds.jpa.entities.JpaNamedFileIdentifierHistory.HistoryType.removed;
15  import static com.trendmicro.grid.acl.ds.jpa.entities.JpaNamedFileIdentifierHistory.HistoryType.renamed;
16  
17  /**
18   * Is the primary handler that operates on the complete dataSets.
19   *
20   * @author juergen_kellerer, 2010-11-22
21   * @version 1.0
22   */
23  public class ReceivedDataSetsHandler extends AbstractReceivedFileContentsHandler<Void> {
24  
25  	private static final Logger log = LoggerFactory.getLogger(ReceivedDataSetsHandler.class);
26  
27  	private final JpaPackageRepository packageRepository;
28  	private final JpaSourceRepository sourceRepository;
29  
30  	private final ReceivedPreparedPackagesHandler packagesHandler;
31  	private final ReceivedSourcesHandler sourcesHandler;
32  
33  	/**
34  	 * Creates a new thread safe instance.
35  	 *
36  	 * @param fileRepository    the repository to use for file contents.
37  	 * @param packageRepository the repository to use for packages.
38  	 * @param packagesHandler   the PreparedPackage handler to use.
39  	 * @param sourcesHandler    the SourcesHandler to use.
40  	 */
41  	public ReceivedDataSetsHandler(JpaFileRepository fileRepository,
42  	                               JpaPackageRepository packageRepository,
43  	                               ReceivedPreparedPackagesHandler packagesHandler,
44  	                               ReceivedSourcesHandler sourcesHandler) {
45  		super(fileRepository);
46  
47  		this.packageRepository = packageRepository;
48  		this.packagesHandler = packagesHandler;
49  		this.sourcesHandler = sourcesHandler;
50  		sourceRepository = sourcesHandler.getSourceRepository();
51  	}
52  
53  	/**
54  	 * {@inheritDoc}
55  	 */
56  	@Override
57  	protected Void handle(StorageContext context) {
58  		final Collection<PreparedPackage> preparedPackages = packagesHandler.handle(context);
59  		final Iterator<ProcessPackageDataSet> dataSets = context.receivedDataSets.iterator();
60  		final Iterator<List<JpaSource>> sourceReferences = sourcesHandler.handle(context).iterator();
61  
62  		for (PreparedPackage preparedPackage : preparedPackages) {
63  			final List<JpaSource> sources = sourceReferences.next();
64  			final ProcessPackageDataSet dataSet = dataSets.next();
65  			if (preparedPackage != null)
66  				handlePreparedPackage(context, preparedPackage, sources, dataSet);
67  		}
68  
69  		return null;
70  	}
71  
72  	/**
73  	 * Handles all linkages between package and other contents and triggers the final update on the prepared package.
74  	 *
75  	 * @param context          the context for the current transaction.
76  	 * @param preparedPackage  the prepare package to handle.
77  	 * @param sourceReferences the list of sources to link the file content entries against.
78  	 * @param dataSet          the relevant dataset.
79  	 */
80  	void handlePreparedPackage(final StorageContext context,
81  	                           final PreparedPackage preparedPackage,
82  	                           final List<JpaSource> sourceReferences,
83  	                           final ProcessPackageDataSet dataSet) {
84  
85  		// Handle the package members.
86  		final Collection<JpaFileDetails> fileRefs = createOrUpdateFileReferences(context, dataSet.getPackageMember());
87  		final boolean revisionNeedsIncrement = synchronizeMembers(context, preparedPackage, fileRefs, dataSet);
88  
89  		// Handle the source linkage
90  		fileRefs.add(preparedPackage.getFileReference());
91  		sourceRepository.referenceFilesFromSources(fileRefs, sourceReferences);
92  
93  		// Handle the linkage against the root package
94  		linkWithRootPackage(context, preparedPackage);
95  
96  		preparedPackage.applyFinalPackageUpdates(revisionNeedsIncrement);
97  
98  		// Flushing the changes now 
99  		// (to avoid duplicate key issues with repeated content in the same transaction)
100 		context.em.flush();
101 	}
102 
103 	/**
104 	 * Manages updates to FILE_CONTENTS for package members and deals with the PACKAGE_FILES & HISTORY table.
105 	 *
106 	 * @param context
107 	 * @param preparedPackage the reference package (including the primary key).
108 	 * @param fileRefs        the list of updated file references (including pk).
109 	 * @param dataSet         the complete dataset containing the package members.
110 	 * @return returns true if history entries were created on the PACKAGE_FILES_HISTORY table.
111 	 */
112 	boolean synchronizeMembers(final StorageContext context,
113 	                           final PreparedPackage preparedPackage,
114 	                           final Collection<JpaFileDetails> fileRefs,
115 	                           final ProcessPackageDataSet dataSet) {
116 
117 		// Updating changes to the file details.
118 		final Iterator<JpaFileDetails> fileRefsIterator = fileRefs.iterator();
119 
120 		// Updating file assignments now.
121 		final JpaPackageDetails packageRef = preparedPackage.getReference();
122 		boolean historyEntriesCreated = false;
123 		final int revision = packageRef.getRevision();
124 
125 		final Map<NamedFileIdentifier, JpaNamedFileIdentifier> namedFileIdentifierMap =
126 				packageRepository.getFilesContainedInPackage(packageRef);
127 		final Map<FileIdentifier, Map<NamedFileIdentifier, JpaNamedFileIdentifier>> fileIdentifierMap =
128 				buildIdentifierOnlyMap(namedFileIdentifierMap);
129 
130 		final int memberCount = dataSet.getPackageMember().size();
131 		final Set<JpaNamedFileIdentifier> persistedOrChangedRefs = new HashSet<JpaNamedFileIdentifier>(memberCount);
132 		final IdentityHashMap<Object, Object> ignoredRefs = new IdentityHashMap<Object, Object>(memberCount);
133 
134 		// Handle additions and renames
135 		Query fnUpdateQuery = null;
136 		for (PackageMember member : dataSet.getPackageMember()) {
137 			final JpaFileDetails fileRef = fileRefsIterator.next();
138 			final NamedFileIdentifier namedIdentifier = member.getIdentifier();
139 			final String fileName = namedIdentifier.getFileName();
140 
141 			JpaNamedFileIdentifier ref = namedFileIdentifierMap.get(namedIdentifier);
142 			if (ref == null) {
143 				final FileIdentifier key = new FileIdentifier(namedIdentifier.getSHA1Hash());
144 				ref = findRenamedReference(namedIdentifier, fileIdentifierMap.get(key));
145 
146 				if (ref == null) {
147 					// new file
148 					ref = new JpaNamedFileIdentifier(fileRef, packageRef, fileName);
149 
150 					if (persistedOrChangedRefs.contains(ref)) {
151 						log.warn("Ignoring a duplicate file reference for file '{}', " +
152 								"this is very likely invalid data in the received data-set.", fileName);
153 						continue;
154 					}
155 
156 					if (log.isTraceEnabled())
157 						log.trace("Creating new package file reference for file '{}'", fileName);
158 
159 					context.em.persist(ref);
160 					mapIdentifier(namedIdentifier, ref, fileIdentifierMap);
161 					persistedOrChangedRefs.add(ref);
162 
163 				} else {
164 					// renamed file
165 					if (persistedOrChangedRefs.contains(ref)) {
166 						log.warn("Ignoring a duplicate request to rename file '{}' => '{}', " +
167 								"this is very likely invalid data in the received data-set.",
168 								ref.getReference().getFileName(), fileName);
169 						continue;
170 					}
171 					persistedOrChangedRefs.add(ref);
172 
173 					ignoredRefs.put(ref, ref);
174 					JpaNamedFileIdentifierHistory history = new JpaNamedFileIdentifierHistory(
175 							fileRef, packageRef, ref.getReference().getFileName(), renamed, revision);
176 
177 					//If disabled, recording of data in the database are ignored. Default is enabled
178 					if (context.enableHistoryRecording) {
179 						if (log.isTraceEnabled()) {
180 							log.trace("Storing history entry for renamed file '{}' => '{}'",
181 									ref.getReference().getFileName(), fileName);
182 						}
183 						context.em.persist(history);
184 					} else {
185 						if (log.isTraceEnabled())
186 							log.trace("enableHistoryRecording is set to {}. Disregarding package-file history entry: {}", context.enableHistoryRecording, history);
187 					}
188 
189 					historyEntriesCreated = true;
190 
191 					// .. and finally updating the name inside the DB
192 					if (fnUpdateQuery == null)
193 						fnUpdateQuery = context.em.createNamedQuery("NamedFileIdentifier.UpdateFileNameById");
194 					int updateCount = fnUpdateQuery.setParameter("fileName", fileName).
195 							setParameter("id", ref.getReference()).executeUpdate();
196 					if (updateCount != 1) {
197 						// A failed rename is fatal as it will prevent history updates in the future!
198 						throw new IllegalStateException("Failed to rename file '" +
199 								ref.getReference().getFileName() + "' to '" + fileName + "' for package '" +
200 								packageRef.getPackageInformation().getName() + "'");
201 					}
202 				}
203 			} else
204 				ignoredRefs.put(ref, ref);
205 		}
206 
207 		// Handle removed files (only if package is not tagged "oversized")
208 		PackageInformation info = dataSet.getProcessedPackage().getPackageInformation();
209 		boolean packageIsOversized = info.containsTag(WellKnownTags.oversized.name());
210 		if (!packageIsOversized) {
211 			for (JpaNamedFileIdentifier ref : namedFileIdentifierMap.values()) {
212 				if (ignoredRefs.containsKey(ref))
213 					continue;
214 
215 				JpaNamedFileIdentifierHistory history = new JpaNamedFileIdentifierHistory(
216 						ref.getReference().getFileDetails(), packageRef, ref.getReference().getFileName(),
217 						removed, revision);
218 				if (log.isTraceEnabled())
219 
220 
221 					//If disabled, recording of data in the database are ignored. Default is enabled
222 					if (context.enableHistoryRecording) {
223 						if (log.isTraceEnabled())
224 							log.trace("Storing history entry for the removed file '{}'", ref.getReference().getFileName());
225 						context.em.persist(history);
226 					} else {
227 						if (log.isTraceEnabled())
228 							log.trace("enableHistoryRecording is set to {}. Disregarding package-file history entry: {}", context.enableHistoryRecording, history);
229 					}
230 
231 				context.em.remove(ref);
232 				historyEntriesCreated = true;
233 			}
234 		}
235 
236 		return historyEntriesCreated;
237 	}
238 
239 	/**
240 	 * Finds the reference that was renamed inside the map of existing identifiers.
241 	 *
242 	 * @param newIdentifier The new identifier to test for a renamed file.
243 	 * @param map           The existing identifiers map (should be pre-filtered using
244 	 *                      {@code buildIdentifierOnlyMap(..)}).
245 	 * @return The renamed existing named reference or 'null' if either no identifier
246 	 *         matched or the identifier was not renamed.
247 	 */
248 	private JpaNamedFileIdentifier findRenamedReference(NamedFileIdentifier newIdentifier, Map<NamedFileIdentifier, JpaNamedFileIdentifier> map) {
249 
250 		if (map == null || map.isEmpty()) return null;
251 
252 		final String newFileName = newIdentifier.getFileName();
253 		for (Map.Entry<NamedFileIdentifier, JpaNamedFileIdentifier> entry : map.entrySet()) {
254 			final NamedFileIdentifier identifier = entry.getKey();
255 
256 			if (!identifier.identifierEquals(newIdentifier)) continue;
257 
258 			boolean pathIsSameButCaseChange = !newFileName.equals(identifier.getFileName()) && newFileName.equalsIgnoreCase(identifier.getFileName());
259 			if (checkOnlyNameDiffers(newFileName, identifier.getFileName()) || pathIsSameButCaseChange)
260 				return entry.getValue();
261 		}
262 
263 		return null;
264 	}
265 
266 	/**
267 	 * Compares path A and B and returns true if everything is equal but the last element differs.
268 	 *
269 	 * @param pathA The first path to check.
270 	 * @param pathB The second path to check.
271 	 * @return true if everything is equal but the last element differs.
272 	 */
273 	boolean checkOnlyNameDiffers(String pathA, String pathB) {
274 		//Todo: Create unit test someday
275 		final StringTokenizer ta = new StringTokenizer(pathA == null ? "" : pathA, PATH_DELIMITERS),
276 				tb = new StringTokenizer(pathB == null ? "" : pathB, PATH_DELIMITERS);
277 
278 		while (ta.hasMoreTokens() && tb.hasMoreTokens()) {
279 			if (!ta.nextToken().equals(tb.nextToken()))
280 				return !ta.hasMoreTokens() && !tb.hasMoreTokens();
281 		}
282 
283 		return false; // if we end here, pathA and B is the same.
284 	}
285 
286 	/**
287 	 * Creates an additional map that doesn't treat file-names in order to detect renamed files.
288 	 *
289 	 * @param namedFileIdentifierMap the source map to convert.
290 	 * @return a map that maps only the sha1 hashes, not the filenames.
291 	 */
292 	private Map<FileIdentifier, Map<NamedFileIdentifier, JpaNamedFileIdentifier>> buildIdentifierOnlyMap(
293 			Map<NamedFileIdentifier, JpaNamedFileIdentifier> namedFileIdentifierMap) {
294 
295 		Map<FileIdentifier, Map<NamedFileIdentifier, JpaNamedFileIdentifier>> map =
296 				new HashMap<FileIdentifier, Map<NamedFileIdentifier, JpaNamedFileIdentifier>>(
297 						namedFileIdentifierMap.size());
298 
299 		for (Map.Entry<NamedFileIdentifier, JpaNamedFileIdentifier> entry : namedFileIdentifierMap.entrySet())
300 			mapIdentifier(entry.getKey(), entry.getValue(), map);
301 
302 		return map;
303 	}
304 
305 	private void mapIdentifier(NamedFileIdentifier namedIdentifier, JpaNamedFileIdentifier reference,
306 	                           Map<FileIdentifier, Map<NamedFileIdentifier, JpaNamedFileIdentifier>> map) {
307 		final FileIdentifier key = new FileIdentifier(namedIdentifier.getSHA1Hash());
308 
309 		Map<NamedFileIdentifier, JpaNamedFileIdentifier> identifierMap = map.get(key);
310 		if (identifierMap == null) {
311 			// Using a singleton map when we have only one entry as
312 			// this greatly reduced memory footprint (and performance)
313 			map.put(key, Collections.singletonMap(namedIdentifier, reference));
314 			return;
315 		} else if (identifierMap.size() == 1)
316 			map.put(key, identifierMap = new LinkedHashMap<NamedFileIdentifier, JpaNamedFileIdentifier>(identifierMap));
317 
318 		identifierMap.put(namedIdentifier, reference);
319 	}
320 
321 	/**
322 	 * Handles the linkage of a package against the root package.
323 	 *
324 	 * @param context         the context for the current transaction.
325 	 * @param preparedPackage the prepare package to link.
326 	 */
327 	void linkWithRootPackage(final StorageContext context, final PreparedPackage preparedPackage) {
328 		final JpaPackageDetails rootPackageRef = context.getRootPackageReference(packageRepository);
329 		final String remoteFileName = preparedPackage.getRemoteFileName();
330 
331 		if (rootPackageRef != null) {
332 			final JpaFileDetails fileReference = preparedPackage.getFileReference();
333 
334 			final List r = context.em.createNamedQuery("NamedFileIdentifier.SelectEntryExists").
335 					setParameter("package", rootPackageRef).
336 					setParameter("file", fileReference).
337 					setParameter("fileName", remoteFileName).setMaxResults(1).getResultList();
338 
339 			if (r.isEmpty()) {
340 				JpaNamedFileIdentifier nfi = new JpaNamedFileIdentifier(fileReference, rootPackageRef, remoteFileName);
341 				context.em.persist(nfi);
342 			}
343 		} else {
344 			if (log.isTraceEnabled())
345 				log.trace("Not inserting linkage to ROOT package with filename '{}', " +
346 						"no ROOT package exists inside the PACKAGES table.", remoteFileName);
347 		}
348 	}
349 }