1   package com.trendmicro.grid.acl.ds.cifs;
2   
3   import com.trendmicro.grid.acl.ds.FileContentRepository;
4   import net.sf.tinyjee.config.PropertySection;
5   import net.sf.tinyjee.util.Hex;
6   import org.slf4j.Logger;
7   import org.slf4j.LoggerFactory;
8   
9   import javax.annotation.PostConstruct;
10  import java.io.FileNotFoundException;
11  import java.io.IOException;
12  import java.io.InputStream;
13  import java.io.OutputStream;
14  import java.net.URI;
15  import java.nio.ByteBuffer;
16  import java.security.DigestOutputStream;
17  import java.security.MessageDigest;
18  import java.security.NoSuchAlgorithmException;
19  import java.util.*;
20  import java.util.concurrent.atomic.AtomicLong;
21  
22  import static com.trendmicro.grid.acl.ds.cifs.CIFSPropertySection.*;
23  import static net.sf.tinyjee.util.Assert.assertEquals;
24  import static net.sf.tinyjee.util.Assert.assertNotNull;
25  
26  /**
27   * Is the shared common code
28   *
29   * @author juergen_kellerer, 2010-05-06
30   * @version 1.0
31   */
32  public abstract class AbstractStructuredFileRepository implements FileContentRepository {
33  
34  	private static final Logger log = LoggerFactory.getLogger(AbstractStructuredFileRepository.class);
35  
36  
37  	private MessageDigest templateHashDigest;
38  	private int temporaryHashLength, hashLength;
39  
40  	private final Random temporaryHashRandom = new Random();
41  	private final long temporaryHashSeed = System.currentTimeMillis();
42  	private final AtomicLong temporaryHashIndex = new AtomicLong();
43  
44  	private final String pathSeparator = "/";//System.getProperty("file.separator");
45  
46  	int[] pathScheme;
47  	String fileExtension = "";
48  	private String baseURIPrefix = "";
49  	private String temporaryPathPrefix = "temporary";
50  
51  
52  	protected abstract boolean isExisting(String path);
53  
54  	protected abstract Date getLastModified(String path);
55  
56  	protected abstract OutputStream openForWrite(String path)
57  			throws FileNotFoundException, IllegalStateException;
58  
59  	protected abstract InputStream openForRead(String path)
60  			throws FileNotFoundException;
61  
62  	protected abstract void rename(String fromPath, String toPath)
63  			throws FileNotFoundException, IllegalArgumentException;
64  
65  	protected abstract String getBaseURI(String uriPrefix);
66  
67  	/**
68  	 * Initializes the class state from values of the section from the config file.
69  	 *
70  	 * @throws NoSuchAlgorithmException in case of the configured hash algorithm is not supported.
71  	 */
72  	@PostConstruct
73  	public void initialize() throws NoSuchAlgorithmException {
74  		PropertySection section = CIFSPropertySection.getPropertySection();
75  		initialize(section);
76  	}
77  
78  	/**
79  	 * Initializes the class state from values of the section from the config file.
80  	 *
81  	 * @param section the configuration file section.
82  	 * @throws NoSuchAlgorithmException in case of the configured hash algorithm is not supported.
83  	 */
84  	protected void initialize(PropertySection section) throws NoSuchAlgorithmException {
85  		String algorithm = section.getPropertyValue(KEY_FILENAME_HASH_KEY_ALGORITHM, "SHA1");
86  		templateHashDigest = MessageDigest.getInstance(algorithm);
87  		hashLength = templateHashDigest.getDigestLength();
88  		temporaryHashLength = hashLength + 2;
89  		log.info("TMACL-00880:Using {} as filename hash key algorithm. " +
90  				"Resulting file paths will have a depth of {} levels.",
91  				templateHashDigest.getAlgorithm(), hashLength / 2);
92  
93  
94  		baseURIPrefix = section.getPropertyValue(KEY_INTERNAL_URI_PREFIX, baseURIPrefix);
95  		if (baseURIPrefix.isEmpty()) {
96  			log.warn("TMACL-00850:No internal URI prefix is defined to address the file repository. " +
97  					"Services that depend on a valid internal URIs may not be able access stored content.");
98  		} else {
99  			log.info("TMACL-00870:Internal URIs pointing to the file " +
100 					"repository are prefixed/start with {}", baseURIPrefix);
101 		}
102 
103 		fileExtension = section.getPropertyValue(KEY_FILENAME_EXTENSION, fileExtension);
104 		if (fileExtension.isEmpty()) {
105 			log.info("TMACL-00890:No filename extension is configured, directories and files may " +
106 					"overlap if the hashing scheme is changed at a later stage.");
107 		} else {
108 			log.info("TMACL-00900:Files are identified inside the file repository " +
109 					"using the extension '{}'", fileExtension);
110 		}
111 
112 		createPathScheme(section);
113 	}
114 
115 	void createPathScheme(PropertySection section) {
116 		String pathSchemePattern = section.getPropertyValue(KEY_FILENAME_PATH_SCHEMA, "");
117 		if (pathSchemePattern.isEmpty()) {
118 			pathScheme = new int[64];
119 			Arrays.fill(pathScheme, 2);
120 		} else {
121 			List<Integer> schemeBuffer = new ArrayList<Integer>();
122 			for (String s : pathSchemePattern.split("/")) {
123 				int increment = (int) Math.ceil(s.length() / 2D);
124 				if (increment > 0)
125 					schemeBuffer.add(increment);
126 			}
127 			pathScheme = new int[schemeBuffer.size()];
128 			for (int i = 0; i < schemeBuffer.size(); i++)
129 				pathScheme[i] = schemeBuffer.get(i);
130 
131 			log.info("TMACL-01500:Using a customized filename path scheme from '{}' is mapped as '{}'",
132 					pathSchemePattern, schemeBuffer);
133 		}
134 	}
135 
136 	/**
137 	 * Converts the given hash to a valid file path.
138 	 *
139 	 * @param hash		 the hash to convert.
140 	 * @param isProperties specifies whether the path should point to the content or properties file.
141 	 * @return a valid file path, pointing to the resource that is specified by the hash.
142 	 */
143 	protected final String convertToPath(byte[] hash, boolean isProperties) {
144 		assertNotNull("hash", hash);
145 		final StringBuilder path = new StringBuilder((hash.length * 2) + 16);
146 		try {
147 			if (hash.length == hashLength) {
148 				// Build formatted hash based path.
149 				int j = 0, i, partLength;
150 				for (i = 0; i < hash.length && j < pathScheme.length;) {
151 					if (i != 0)
152 						path.append(pathSeparator);
153 					partLength = pathScheme[j++];
154 					Hex.encode(path, hash, i, Math.min(partLength, hash.length - i));
155 					i += partLength;
156 				}
157 
158 				// Adding remaining path elements.
159 				if (i < hash.length) {
160 					if (path.length() != 0)
161 						path.append(pathSeparator);
162 					Hex.encode(path, hash, i, hash.length - i);
163 				}
164 
165 			} else if (hash.length == temporaryHashLength) {
166 				Hex.encode(path.append(temporaryPathPrefix).append(pathSeparator), hash);
167 			} else
168 				throw new IllegalArgumentException("The specified hash doesn't seem to refer to a valid resource.");
169 
170 			if (isProperties)
171 				path.append(".info");
172 			else if (!fileExtension.isEmpty())
173 				path.append(fileExtension);
174 
175 			return path.toString();
176 
177 		} catch (IOException e) {
178 			throw new RuntimeException(e);
179 		}
180 	}
181 
182 	protected final byte[] convertFromPath(String path) {
183 		if (path.endsWith(".info"))
184 			path = path.substring(0, path.length() - 5);
185 		if (!fileExtension.isEmpty() && path.endsWith(fileExtension))
186 			path = path.substring(0, path.length() - fileExtension.length());
187 		if (path.startsWith(temporaryPathPrefix))
188 			path = path.substring(temporaryPathPrefix.length());
189 
190 		final StringBuilder hash = new StringBuilder(40);
191 		final StringTokenizer tok = new StringTokenizer(path, "\\/");
192 		while (tok.hasMoreTokens())
193 			hash.append(tok.nextToken());
194 
195 		return Hex.decode(hash);
196 	}
197 
198 	private void assertIsValidHashKey(byte[] hashKey) {
199 		assertNotNull("hashKey", hashKey);
200 		if (hashKey.length != hashLength && hashKey.length != temporaryHashLength) {
201 			throw new IllegalArgumentException("The specified hash key is neither a key that identifies " +
202 					"a temporary nor a normal resource.");
203 		}
204 	}
205 
206 	public int getTemporaryHashLength() {
207 		return temporaryHashLength;
208 	}
209 
210 	public int getHashLength() {
211 		return hashLength;
212 	}
213 
214 	/**
215 	 * {@inheritDoc}
216 	 */
217 	public final String getHashKeyAlgorithm() {
218 		return templateHashDigest.getAlgorithm();
219 	}
220 
221 	/**
222 	 * {@inheritDoc}
223 	 */
224 	public final MessageDigest newHashKeyDigest() {
225 		try {
226 			return (MessageDigest) templateHashDigest.clone();
227 		} catch (CloneNotSupportedException e) {
228 			throw new RuntimeException(e);
229 		}
230 	}
231 
232 	/**
233 	 * {@inheritDoc}
234 	 */
235 	public void attachProperties(byte[] hashKey, Map<String, String> properties) throws IOException {
236 		if (!isExisting(hashKey)) {
237 			throw new FileNotFoundException("Cannot store properties " + properties + " for " +
238 					Hex.encode(hashKey) + ", no content exists under this identifier.");
239 		}
240 
241 		String propertiesPath = convertToPath(hashKey, true);
242 
243 		OutputStream out = openForWrite(propertiesPath);
244 		try {
245 			Properties p = new Properties();
246 			p.putAll(properties);
247 			p.store(out, "");
248 		} finally {
249 			out.close();
250 		}
251 	}
252 
253 	/**
254 	 * {@inheritDoc}
255 	 */
256 	@SuppressWarnings("unchecked")
257 	public Map<String, String> readProperties(byte[] hashKey) throws IOException {
258 		if (!isExisting(hashKey)) {
259 			throw new FileNotFoundException("Cannot load properties for " +
260 					Hex.encode(hashKey) + ", no content exists under this identifier.");
261 		}
262 
263 		String propertiesPath = convertToPath(hashKey, true);
264 
265 		try {
266 			InputStream in = openForRead(propertiesPath);
267 			try {
268 				Properties p = new Properties();
269 				p.load(in);
270 				return (Map) p;
271 			} finally {
272 				in.close();
273 			}
274 		} catch (FileNotFoundException e) {
275 			return null;
276 		}
277 	}
278 
279 	/**
280 	 * {@inheritDoc}
281 	 */
282 	public final boolean isExisting(byte[] hashKey) {
283 		String path = convertToPath(hashKey, false);
284 		return isExisting(path);
285 	}
286 
287 	/**
288 	 * {@inheritDoc}
289 	 */
290 	public final Date getLastModified(byte[] hashKey) {
291 		String path = convertToPath(hashKey, false);
292 		return getLastModified(path);
293 	}
294 
295 	/**
296 	 * {@inheritDoc}
297 	 */
298 	public final DigestOutputStream openForWrite(byte[] hashKey) throws FileNotFoundException, IllegalStateException {
299 		String path = convertToPath(hashKey, false);
300 		return new DigestOutputStream(openForWrite(path), newHashKeyDigest());
301 	}
302 
303 	/**
304 	 * {@inheritDoc}
305 	 */
306 	public final InputStream openForRead(byte[] hashKey) throws FileNotFoundException {
307 		String path = convertToPath(hashKey, false);
308 		return openForRead(path);
309 	}
310 
311 	/**
312 	 * {@inheritDoc}
313 	 * <p/>
314 	 * Renaming is only legal when done from a temporary to a normal resource.
315 	 */
316 	public final void rename(byte[] fromHashKey, byte[] toHashKey)
317 			throws FileNotFoundException, IllegalArgumentException {
318 
319 		// Ensure the input values are correct, throw IllegalArgumentException if not.
320 		assertNotNull("fromHashKey", fromHashKey);
321 		assertNotNull("toHashKey", toHashKey);
322 		assertEquals("fromHashKey#length", temporaryHashLength, fromHashKey.length);
323 		assertEquals("toHashKey#length", hashLength, toHashKey.length);
324 
325 		String fromPath = convertToPath(fromHashKey, false);
326 		String toPath = convertToPath(toHashKey, false);
327 		rename(fromPath, toPath);
328 	}
329 
330 	/**
331 	 * {@inheritDoc}
332 	 */
333 	public final byte[] createTemporaryHashKey() {
334 		ByteBuffer key = ByteBuffer.allocate(temporaryHashLength);
335 		temporaryHashRandom.nextBytes(key.array());
336 		key.putLong(temporaryHashSeed);
337 		key.putLong(temporaryHashIndex.incrementAndGet());
338 		return key.array();
339 	}
340 
341 	/**
342 	 * {@inheritDoc}
343 	 */
344 	public URI encodeToInternalURI(byte[] hashKey) {
345 		assertIsValidHashKey(hashKey);
346 		return URI.create(getBaseURI(baseURIPrefix) + convertToPath(hashKey, false));
347 	}
348 
349 	/**
350 	 * {@inheritDoc}
351 	 */
352 	public byte[] decodeFromInternalURI(URI internalURI) {
353 		String baseURI = getBaseURI(baseURIPrefix), uri = internalURI.toString();
354 		if (!uri.startsWith(baseURI))
355 			return null;
356 		return convertFromPath(uri.substring(baseURI.length()));
357 	}
358 
359 	/**
360 	 * {@inheritDoc}
361 	 */
362 	public final String encodeHashKey(byte[] hashKey) {
363 		assertIsValidHashKey(hashKey);
364 		return Hex.encode(hashKey);
365 	}
366 
367 	/**
368 	 * {@inheritDoc}
369 	 */
370 	public final byte[] decodeHashKey(String hashKey) {
371 		byte[] hash = Hex.decode(hashKey);
372 		assertIsValidHashKey(hash);
373 		return hash;
374 	}
375 }