/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sling.installer.core.impl; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.math.BigInteger; import java.security.MessageDigest; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for all file handling. */ public class FileDataStore { private static final Logger log = LoggerFactory.getLogger(FileDataStore.class); /** * The name of the bundle context property defining the location for the * installer files (value is "sling.installer.dir"). */ private static final String CONFIG_DIR = "sling.installer.dir"; /** * The default configuration data directory if no location is configured * (value is "installer"). */ private static final String DEFAULT_DIR = "installer"; private final File directory; /** Public instance - to avoid passing a reference to this service to each data object. */ public static FileDataStore SHARED; /** Cache for url to digest mapping. */ private final Map<String, CacheEntry> digestCache = new HashMap<String, CacheEntry>(); private static final class CacheEntry { public final File file; public final String digest; public CacheEntry(final File file, final String digest) { this.file = file; this.digest = digest; } } /** * Create a file util instance and detect the installer directory. */ public FileDataStore( final BundleContext bundleContext ) { String location = bundleContext.getProperty(CONFIG_DIR); // no configured location, use the config dir in the bundle persistent // area if ( location == null ) { final File locationFile = bundleContext.getDataFile( DEFAULT_DIR ); if ( locationFile != null ) { location = locationFile.getAbsolutePath(); } } // fall back to the current working directory if the platform does // not support filesystem based data area if ( location == null ) { location = System.getProperty( "user.dir" ) + File.separatorChar + DEFAULT_DIR; } // ensure the file is absolute File locationFile = new File( location ); if ( !locationFile.isAbsolute() ) { final File bundleLocationFile = bundleContext.getDataFile( locationFile.getPath() ); if ( bundleLocationFile != null ) { locationFile = bundleLocationFile; } // ensure the file object is an absolute file object locationFile = locationFile.getAbsoluteFile(); } // check the location if ( !locationFile.isDirectory() ) { if ( locationFile.exists() ) { throw new IllegalArgumentException( location + " is not a directory" ); } if ( !locationFile.mkdirs() ) { throw new IllegalArgumentException( "Cannot create directory " + location ); } } this.directory = locationFile; SHARED = this; log.debug("FileDataStore setup with directory={}", safePath(directory)); } /** * Return a file with the given name in the installer directory. * @param fileName The file name */ public File getDataFile(final String fileName) { final File result = new File(this.directory, fileName); log.debug("getDataFile({}) returns {}", fileName, safePath(result)); return result; } /** Serial number to create unique file names in the data storage. */ private static long serialNumberCounter = System.currentTimeMillis(); private static long getNextSerialNumber() { synchronized (RegisteredResourceImpl.class) { return serialNumberCounter++; } } /** * Create a new unique data file. */ public File createNewDataFile(final InputStream stream, final String url, final String digest, final String hint) throws IOException { // check if we already have this data if ( digest != null ) { synchronized ( this.digestCache ) { final CacheEntry storedDigest = this.digestCache.get(url); if ( storedDigest != null && storedDigest.digest.equals(digest) ) { log.debug( "File {} with digest {} found, returning {}", url, digest, safePath(storedDigest.file)); return storedDigest.file; } } } final int pos = url.lastIndexOf('/'); final String name = url.substring(pos + 1); final String filename = (hint == null ? "rsrc" : hint) + '-' + name + '-' + getNextSerialNumber() + ".ser"; //replace special characters from the filename that are not allowed by the OS final String filename2 = filename.replaceAll("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+", "_"); // Windows final File file = this.getDataFile(filename2); this.copyToLocalStorage(stream, file); log.debug("Stream with digest {} copied to {}", digest, safePath(file)); if ( digest != null ) { synchronized ( this.digestCache ) { this.digestCache.put(url, new CacheEntry(file, digest)); } } return file; } public void updateDigestCache(final String url, final File file, final String digest) { log.debug("Updating digest cache for {}, file {}, digest {}", url, safePath(file), digest); synchronized ( this.digestCache ) { this.digestCache.put(url, new CacheEntry(file, digest)); } } /** * Copy data to local storage. */ protected void copyToLocalStorage(final InputStream data, final File dataFile) throws IOException { final OutputStream os = new BufferedOutputStream(new FileOutputStream(dataFile)); try { final byte[] buffer = new byte[16384]; int count = 0; while( (count = data.read(buffer, 0, buffer.length)) > 0) { os.write(buffer, 0, count); } os.flush(); } finally { os.close(); } } public File createNewDataFile(final String hint, final InputStream stream) throws IOException { final String filename = (hint == null ? "unknown" : hint) + "-resource-" + getNextSerialNumber() + ".ser"; final File file = this.getDataFile(filename); log.debug("createNewDataFile: file={}", safePath(file)); this.copyToLocalStorage(stream, file); return file; } public void removeFromDigestCache(final String url, final String digest) { log.debug("Removing {} / {} from digest cache", url, digest); synchronized ( this.digestCache ) { final CacheEntry entry = this.digestCache.get(url); if ( entry != null && entry.digest.equals(digest) ) { this.digestCache.remove(url); } } } /** Digest is needed to detect changes in data */ public static String computeDigest(final File data) throws IOException { try { final InputStream is = new FileInputStream(data); try { final MessageDigest d = MessageDigest.getInstance("MD5"); final byte[] buffer = new byte[8192]; int count = 0; while( (count = is.read(buffer, 0, buffer.length)) > 0) { d.update(buffer, 0, count); } final String result = digestToString(d); log.debug("Digest of {} is {}", safePath(data), result); return result; } finally { is.close(); } } catch (IOException ioe) { throw ioe; } catch (Exception ignore) { final String result = data.toString(); log.debug("Returning fake digest {} for {} due to {}", result, safePath(data), ignore); return result; } } /** convert digest to readable string (http://www.javalobby.org/java/forums/t84420.html) */ private static String digestToString(MessageDigest d) { final BigInteger bigInt = new BigInteger(1, d.digest()); return new String(bigInt.toString(16)); } /** Digest is needed to detect changes in data, and must not depend on dictionary ordering */ public static String computeDigest(Dictionary<String, Object> data) { try { final MessageDigest d = MessageDigest.getInstance("MD5"); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); final ObjectOutputStream oos = new ObjectOutputStream(bos); final SortedSet<String> sortedKeys = new TreeSet<String>(); if (data != null) { for(Enumeration<String> e = data.keys(); e.hasMoreElements(); ) { final String key = e.nextElement(); sortedKeys.add(key); } } for(final String key : sortedKeys) { oos.writeObject(key); final Object val = data.get(key); if ( val instanceof Number ) { oos.writeObject(String.valueOf(val)); } else { oos.writeObject(val); } } oos.flush(); d.update(bos.toByteArray()); final String result = digestToString(d); log.debug("Dictionary digest = {}", result); return result; } catch (Exception ignore) { final String result = data.toString(); log.debug("Returning fake dictionary digest {} due to {}", result, ignore); return result; } } private static final String safePath(File f) { return f == null ? null : f.getAbsolutePath(); } }