package org.webpieces.compiler.impl;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.compiler.api.CompileConfig;
import org.webpieces.util.file.VirtualFile;
/**
* Used to speed up compilation time
*/
public class BytecodeCache {
private static final Logger log = LoggerFactory.getLogger(BytecodeCache.class);
private CompileConfig config;
public BytecodeCache(CompileConfig config) {
this.config = config;
}
/**
* Delete the bytecode
* @param name Cache name
*/
public void deleteBytecode(String name) {
VirtualFile f = cacheFile(name.replace("/", "_").replace("{", "_").replace("}", "_").replace(":", "_"));
if (f.exists()) {
f.delete();
}
}
/**
* Retrieve the bytecode if source has not changed
* @param name The cache name
* @param source The source code
* @return The bytecode
*/
public byte[] getBytecode(String name, String source) {
try {
VirtualFile f = cacheFile(name.replace("/", "_").replace("{", "_").replace("}", "_").replace(":", "_"));
if (f.exists()) {
try (InputStream fis = f.openInputStream()) {
// Read hash
int offset = 0;
int read = -1;
StringBuilder hash = new StringBuilder();
// look for null byte, or end-of file
while ((read = fis.read()) > 0) {
hash.append((char) read);
offset++;
}
if (!hash(source).equals(hash.toString())) {
log.trace(()->"Bytecode too old ("+hash+" != "+hash(source)+") for name="+name);
return null;
}
byte[] byteCode = new byte[(int) f.length() - (offset + 1)];
fis.read(byteCode);
return byteCode;
}
}
log.trace(()->"Cache MISS for "+name);
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Cache the bytecode
* @param byteCode The bytecode
* @param name The cache name
* @param source The corresponding source
*/
public void cacheBytecode(byte[] byteCode, String name, String source) {
try {
VirtualFile f = cacheFile(name.replace("/", "_").replace("{", "_").replace("}", "_").replace(":", "_"));
try (OutputStream fos = f.openOutputStream()) {
fos.write(hash(source).getBytes("utf-8"));
fos.write(0);
fos.write(byteCode);
}
log.trace(()->name + "cached");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Build a hash of the source code.
* To efficiently track source code modifications.
*/
String hash(String text) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(text.getBytes("utf-8"));
byte[] digest = messageDigest.digest();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < digest.length; ++i) {
int value = digest[i];
if (value < 0) {
value += 256;
}
builder.append(Integer.toHexString(value));
}
return builder.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Retrieve the real file that will be used as cache.
*/
VirtualFile cacheFile(String id) {
VirtualFile dir = config.getByteCodeCacheDir();
if (!dir.exists()) {
dir.mkdirs();
}
return dir.child(id);
}
}