package pl.matisoft.soy.ajax.hash;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Optional;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import org.apache.commons.io.input.ReaderInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import pl.matisoft.soy.config.SoyViewConfigDefaults;
/**
* Created with IntelliJ IDEA.
* User: mszczap
* Date: 29.06.13
* Time: 23:58
*
* An MD5 algorithm implementation of a hash file generator
*
* It is important to notice that this implementation supports a dev and prod modes
* in dev (hotReloadMode) mode the implementation will not cache md5 hash checksums, conversely
* in prod mode it will cache it. The cache can be fine tuned via setters.
*/
public class MD5HashFileGenerator implements HashFileGenerator, InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MD5HashFileGenerator.class);
private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE;
private final static int DEF_CACHE_MAX_SIZE = 10000;
private final static String DEF_TIME_UNIT = "DAYS";
private final static int DEF_EXPIRE_AFTER_WRITE = 1;
/** maximum number of entries this cache will hold */
private int cacheMaxSize = DEF_CACHE_MAX_SIZE;
/** number of time units after which once written entries will expire */
private int expireAfterWrite = DEF_EXPIRE_AFTER_WRITE;
/** String used to denote a TimeUnit */
private String expireAfterWriteUnit = DEF_TIME_UNIT;
/**friendly*/ Cache<URL, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(expireAfterWrite, TimeUnit.valueOf(expireAfterWriteUnit))
.maximumSize(cacheMaxSize)
.concurrencyLevel(1) //look up a constant class, 1 is not very clear
.build();
public void afterPropertiesSet() {
cache = CacheBuilder.newBuilder()
.expireAfterWrite(expireAfterWrite, TimeUnit.valueOf(expireAfterWriteUnit))
.maximumSize(cacheMaxSize)
.concurrencyLevel(1) //look up a constant class, 1 is not very clear
.build();
}
/**
* Calculates a md5 hash for an url
*
* If a passed in url is absent then this method will return absent as well
*
* @param url - an url to a soy template file
* @return - md5 checksum of a template file
* @throws IOException - in a case there is an IO error calculating md5 checksum
*/
@Override
public Optional<String> hash(final Optional<URL> url) throws IOException {
if (!url.isPresent()) {
return Optional.absent();
}
logger.debug("Calculating md5 hash, url:{}", url);
if (isHotReloadModeOff()) {
final String md5 = cache.getIfPresent(url.get());
logger.debug("md5 hash:{}", md5);
if (md5 != null) {
return Optional.of(md5);
}
}
final InputStream is = url.get().openStream();
final String md5 = getMD5Checksum(is);
if (isHotReloadModeOff()) {
logger.debug("caching url:{} with hash:{}", url, md5);
cache.put(url.get(), md5);
}
return Optional.fromNullable(md5);
}
@Override
public Optional<String> hashMulti(final Collection<URL> urls) throws IOException {
if (urls.isEmpty()) {
return Optional.absent();
}
if (urls.size() == 1) {
return hash(Optional.of(urls.iterator().next()));
}
final List<String> hashes = new ArrayList<String>();
for (final URL url : urls) {
final Optional<String> hash = hash(Optional.of(url));
if (hash.isPresent()) {
hashes.add(hash.get());
}
}
final StringBuilder builder = new StringBuilder();
for (final String hash : hashes) {
builder.append(hash);
}
return Optional.fromNullable(getMD5Checksum(new ReaderInputStream(new StringReader(builder.toString()))));
}
public static String getMD5Checksum(final InputStream is) throws IOException {
final HashFunction hf = Hashing.md5();
final HashCode hashCode = hf.hashBytes(getBytesFromInputStream(is));
return hashCode.toString();
}
private static byte[] getBytesFromInputStream(final InputStream inStream) throws IOException {
// Get the size of the file
long streamLength = inStream.available();
if (streamLength > Integer.MAX_VALUE) {
// File is too large
}
// Create the byte array to hold the data
byte[] bytes = new byte[(int) streamLength];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead = inStream.read(bytes,
offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
// Ensure all the bytes have been read in
if (offset < bytes.length) {
throw new IOException("Could not completely read file ");
}
// Close the input stream and return bytes
inStream.close();
return bytes;
}
public void setHotReloadMode(boolean hotReloadMode) {
this.hotReloadMode = hotReloadMode;
}
public void setCacheMaxSize(int cacheMaxSize) {
this.cacheMaxSize = cacheMaxSize;
}
public void setExpireAfterWrite(int expireAfterWrite) {
this.expireAfterWrite = expireAfterWrite;
}
public void setExpireAfterWriteUnit(String expireAfterWriteUnit) {
this.expireAfterWriteUnit = expireAfterWriteUnit;
}
private boolean isHotReloadModeOff() {
return !hotReloadMode;
}
public boolean isHotReloadMode() {
return hotReloadMode;
}
public int getCacheMaxSize() {
return cacheMaxSize;
}
public int getExpireAfterWrite() {
return expireAfterWrite;
}
public String getExpireAfterWriteUnit() {
return expireAfterWriteUnit;
}
}