1   package com.trendmicro.grid.acl.ds.jpa;
2   
3   import com.trendmicro.grid.acl.ds.FileProvider;
4   import com.trendmicro.grid.acl.ds.RemoteCacheable;
5   import com.trendmicro.grid.acl.ds.TagQueryExpression;
6   import com.trendmicro.grid.acl.ds.datatypes.SharedFileDetails;
7   import com.trendmicro.grid.acl.ds.datatypes.SharedFileInformation;
8   import com.trendmicro.grid.acl.ds.jpa.entities.JpaFileDetails;
9   import com.trendmicro.grid.acl.ds.jpa.entities.JpaFileIdentifier;
10  import com.trendmicro.grid.acl.ds.jpa.entities.JpaFileInformation;
11  import com.trendmicro.grid.acl.ds.jpa.tagquery.TagQueryProviderSelector;
12  import com.trendmicro.grid.acl.ds.jpa.util.FileQueryConfigurator;
13  import com.trendmicro.grid.acl.l0.datatypes.*;
14  import net.sf.tinyjee.util.Hex;
15  import org.slf4j.Logger;
16  import org.slf4j.LoggerFactory;
17  import org.springframework.stereotype.Repository;
18  import org.springframework.transaction.annotation.Transactional;
19  
20  import javax.annotation.Resource;
21  import javax.persistence.*;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.List;
25  
26  import static com.trendmicro.grid.acl.ds.jpa.util.JpaUtils.*;
27  import static net.sf.tinyjee.util.Assert.assertNotNull;
28  import static net.sf.tinyjee.util.Assert.assertTrue;
29  
30  /**
31   * Implements FileProvider using JPA.
32   *
33   * @author juergen_kellerer, 2010-06-07
34   * @version 1.0
35   */
36  @Repository
37  @Transactional(readOnly = true)
38  public class JpaFileRepository implements FileProvider {
39  
40  	private static final Logger log = LoggerFactory.getLogger(JpaFileRepository.class);
41  
42  	private static int pageSize = 1000;
43  
44  	public static int getPageSize() {
45  		return pageSize;
46  	}
47  
48  	public static void setPageSize(int pageSize) {
49  		JpaFileRepository.pageSize = pageSize;
50  	}
51  
52  	static final Callback<Object[], FileIdentifier> bytesToFileIdentifierConverter = new Callback<Object[], FileIdentifier>() {
53  		@Override
54  		public FileIdentifier call(Object[] hashes) {
55  			return new FileIdentifier(
56  					(byte[]) hashes[hashes.length - 2],
57  					(byte[]) hashes[hashes.length - 1]);
58  		}
59  	};
60  
61  	@PersistenceContext(unitName = "CoreDB")
62  	EntityManager em;
63  
64  	@Resource
65  	TagQueryProviderSelector providerSelector;
66  
67  	@Override
68  	@RemoteCacheable
69  	public Collection<Boolean> isFilesKnown(Collection<FileIdentifier> files) {
70  		final FileQueryConfigurator<Integer> queryConfigurator = new FileQueryConfigurator<Integer>(em, Integer.class, "FileContents.IsFileKnown");
71  
72  		final Collection<Boolean> results = new ArrayList<Boolean>(files.size());
73  		for (FileIdentifier file : files) {
74  			List<Integer> resultList = queryConfigurator.getQuery(file).setMaxResults(1).getResultList();
75  			results.add(!resultList.isEmpty() && resultList.get(0) == 1);
76  		}
77  
78  		return results;
79  	}
80  
81  	@Override
82  	@RemoteCacheable
83  	public Collection<Boolean> isFilesTaggedWith(Collection<FileIdentifier> files, String[] tags) {
84  		// This is a un-optimized version of this method that should not be called.
85  		// The reason that this implementation exists is only to handle the case that caching is disabled.
86  		final Collection<Boolean> results = new ArrayList<Boolean>(files.size());
87  		for (SharedFileInformation info : getFileInformationList(files))
88  			results.add(info == null || info.isUnknown() ? null : isTaggedWith(info, tags));
89  
90  		return results;
91  	}
92  
93  	@Override
94  	public FileIdentiferListPage getMatchingFiles(TagQueryExpression expression, Range range, int pageNumber) {
95  		if (range != null && !(range instanceof DaysRange)) {
96  			log.warn("TMACL-00830:Ignoring CompositeRange in JPA implementation. Such requests should get handled inside the Cache module.");
97  			return null;
98  		}
99  
100 		final TagQueryProvider provider = providerSelector.getSelectedProvider(em);
101 		final Query filesQuery = provider.getMatchingFilesQuery(em, expression, (DaysRange) range);
102 
103 		applyPage(filesQuery, pageNumber, pageSize);
104 
105 		return toListPage(filesQuery, bytesToFileIdentifierConverter, new FileIdentiferListPage());
106 	}
107 
108 	@Override
109 	public Collection<FileIdentifier> getCanonicalIdentifiers(Collection<FileIdentifier> files) {
110 		final FileQueryConfigurator<JpaFileIdentifier> queryConfigurator =
111 				new FileQueryConfigurator<JpaFileIdentifier>(em, JpaFileIdentifier.class, "FileContents.SelectFileIdentifier");
112 
113 		final Collection<FileIdentifier> results = new ArrayList<FileIdentifier>(files.size());
114 		for (FileIdentifier file : files) {
115 			List<JpaFileIdentifier> resultList = queryConfigurator.getQuery(file).getResultList();
116 			results.add(resultList.isEmpty() ? null : resultList.get(0));
117 		}
118 
119 		return results;
120 	}
121 
122 	@Override
123 	@SuppressWarnings("unchecked")
124 	@RemoteCacheable(comment = "Note: Don't use this method for JPA purposes, " +
125 			"the returned entities will not be usable when returned from the remote cache.")
126 	public Collection<SharedFileInformation> getFileInformationList(Collection<FileIdentifier> files) {
127 		return (Collection) getJpaFileInformationList(files);
128 	}
129 
130 	/**
131 	 * Returns the file information list for the given files.
132 	 *
133 	 * @param files The files to return the information list for.
134 	 * @return the file information list for the given file. The list may contain 'null'
135 	 *         entries if the corresponding file was unknown.
136 	 */
137 	public Collection<JpaFileInformation> getJpaFileInformationList(Collection<FileIdentifier> files) {
138 		final FileQueryConfigurator<JpaFileInformation> queryConfigurator =
139 				new FileQueryConfigurator<JpaFileInformation>(em, JpaFileInformation.class, "FileContents.SelectFileInformation");
140 
141 		final Collection<JpaFileInformation> results = new ArrayList<JpaFileInformation>(files.size());
142 		for (FileIdentifier file : files) {
143 			List<JpaFileInformation> resultList = queryConfigurator.getQuery(file).getResultList();
144 			results.add(resultList.isEmpty() ? null : resultList.get(0));
145 		}
146 
147 		return results;
148 	}
149 
150 	@Override
151 	@SuppressWarnings("unchecked")
152 	@RemoteCacheable(comment = "Note: Don't use this method for JPA purposes, " +
153 			"the returned entities will not be usable when returned from the remote cache.")
154 	public Collection<SharedFileDetails> getFileDetailsList(Collection<FileIdentifier> files) {
155 		return (Collection) getJpaFileDetailsList(files);
156 	}
157 
158 	/**
159 	 * Returns the file details list for the given files.
160 	 *
161 	 * @param files The files to return the details list for.
162 	 * @return the file details list for the given file. The list may contain 'null'
163 	 *         entries if the corresponding file was unknown.
164 	 */
165 	public Collection<JpaFileDetails> getJpaFileDetailsList(Collection<FileIdentifier> files) {
166 		final FileQueryConfigurator<JpaFileDetails> queryConfigurator = getDetailsQueryConfigurator();
167 
168 		final Collection<JpaFileDetails> results = new ArrayList<JpaFileDetails>(files.size());
169 		for (FileIdentifier file : files) {
170 			List<JpaFileDetails> resultList = queryConfigurator.getQuery(file).getResultList();
171 			results.add(resultList.isEmpty() ? null : resultList.get(0));
172 		}
173 
174 		return results;
175 	}
176 
177 	/**
178 	 * Creates the specified FILE_CONTENTs entry.
179 	 *
180 	 * @param fileDetails the source data to store.
181 	 * @return the stored entry.
182 	 * @throws EntityExistsException in case of an entity exists under the
183 	 */
184 	public JpaFileDetails createFileDetails(FileDetails fileDetails) throws PersistenceException {
185 		assertNotNull("fileDetails", fileDetails);
186 		assertNotNull("fileDetails#getIdentifier", fileDetails.getIdentifier());
187 		assertTrue(fileDetails.getIdentifier().isCanonical(), "fileDetails#getIdentifier#isCanonical");
188 		assertNotNull("fileDetails#getInformation", fileDetails.getInformation());
189 
190 		JpaFileDetails details = new JpaFileDetails(
191 				new JpaFileIdentifier(fileDetails.getIdentifier(), fileDetails.getMetadata()),
192 				new JpaFileInformation(fileDetails.getInformation()), fileDetails.getMetadata());
193 
194 		em.persist(details);
195 
196 		return details;
197 	}
198 
199 	/**
200 	 * Returns the JpaFileDetails element for the given identifer.
201 	 *
202 	 * @param identifier the identifier for the file content.
203 	 * @return the JpaFileDetails element for the given identifer or 'null' if it does not exist.
204 	 */
205 	public JpaFileDetails getFileDetails(FileIdentifier identifier) {
206 		List<JpaFileDetails> r = getDetailsQueryConfigurator().getQuery(identifier).getResultList();
207 		return r.isEmpty() ? null : r.get(0);
208 	}
209 
210 	/**
211 	 * Returns lazy loaded JpaFileDetails that may be used to satisfy foreign keys with minimum overhead.
212 	 *
213 	 * @param identifiers the identifiers to attach to the JpaFileDetails.
214 	 * @return lazy loaded JpaFileDetails used for references.
215 	 */
216 	public Collection<JpaFileDetails> getReferences(Collection<? extends FileIdentifier> identifiers) {
217 		final FileQueryConfigurator<Integer> configurator = getReferenceQueryConfigurator();
218 		final Collection<JpaFileDetails> results = new ArrayList<JpaFileDetails>();
219 
220 		for (FileIdentifier identifier : identifiers)
221 			results.add(getReference(configurator, identifier));
222 
223 		return results;
224 	}
225 
226 	/**
227 	 * Returns a lazy loaded JpaFileDetails that may be used to satisfy foreign keys with minimum overhead.
228 	 *
229 	 * @param identifier the identifier to attach to the JpaFileDetails.
230 	 * @return a lazy loaded JpaFileDetails used for references.
231 	 */
232 	public JpaFileDetails getReference(FileIdentifier identifier) {
233 		return getReference(getReferenceQueryConfigurator(), identifier);
234 	}
235 
236 	/**
237 	 * Returns a query configurator that configures queries returning FileDetails by FileIdentifiers.
238 	 *
239 	 * @return a query configurator that configures queries returning FileDetails by FileIdentifiers.
240 	 */
241 	public FileQueryConfigurator<JpaFileDetails> getDetailsQueryConfigurator() {
242 		return new FileQueryConfigurator<JpaFileDetails>(em, JpaFileDetails.class, "FileContents.SelectFileDetails");
243 	}
244 
245 	/**
246 	 * Returns a query configurator that configures queries returning primary keys by FileIdentifiers.
247 	 *
248 	 * @return a query configurator that configures queries returning primary keys by FileIdentifiers.
249 	 */
250 	public FileQueryConfigurator<Integer> getReferenceQueryConfigurator() {
251 		return new FileQueryConfigurator<Integer>(em, Integer.class, "FileContents.GetPrimaryKey");
252 	}
253 
254 	/**
255 	 * Returns a lazy loaded JpaFileDetails that may be used to satisfy foreign keys with minimum overhead.
256 	 *
257 	 * @param primaryKeyQueryConfigurator the configurator used for the query.
258 	 * @param identifier                  the identifier to attach to the JpaFileDetails.
259 	 * @return a lazy loaded JpaFileDetails used for references, or 'null' if the entry was missing
260 	 *         and no MD5 hash was defined..
261 	 */
262 	public JpaFileDetails getReference(FileQueryConfigurator<Integer> primaryKeyQueryConfigurator, FileIdentifier identifier) {
263 
264 		final Integer primaryKey = getOrCreateFileContentPrimaryKey(primaryKeyQueryConfigurator, identifier);
265 		return primaryKey == null ? null : em.getReference(JpaFileDetails.class, primaryKey);
266 	}
267 
268 	Integer getOrCreateFileContentPrimaryKey(FileQueryConfigurator<Integer> primaryKeyQueryConfigurator, FileIdentifier identifier) {
269 
270 		final byte[] sha1 = identifier.getSHA1Hash();
271 		final List<Integer> fileContentKeyResult = primaryKeyQueryConfigurator.getQuery(identifier).getResultList();
272 
273 		if (fileContentKeyResult.isEmpty()) {
274 			final byte[] md5Hash = identifier.getMD5Hash();
275 			if (md5Hash != null) {
276 				final JpaFileDetails unknownFileContent = new JpaFileDetails(
277 						new JpaFileIdentifier(sha1, md5Hash),
278 						new JpaFileInformation(true), null);
279 
280 				if (log.isDebugEnabled()) log.debug("Creating new 'unknown' file content entry for file sha1={}", Hex.encode(sha1));
281 
282 				em.persist(unknownFileContent);
283 				return unknownFileContent.getPrimaryKey();
284 			}
285 			return null;
286 		} else {
287 			return fileContentKeyResult.get(0);
288 		}
289 	}
290 }