// Copyright (C) 2011 Google Inc.
//
// Licensed 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 com.google.caja.precajole;
import com.google.caja.SomethingWidgyHappenedError;
import com.google.caja.parser.js.CajoledModule;
import com.google.caja.util.Lists;
import com.google.caja.util.Maps;
import com.google.caja.util.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Hex;
/**
* This is a PrecajoleMap that looks up precajoled modules from a
* directory in the caja jar (or in the filesystem).
* <p>
* The directory contains serialized baked CajoledModules, stored
* one per file. Filenames are hex-encoded sha1 key of the uncajoled
* source text.
* <p>
* There's also a file index.dat that contains a serialized map
* of URI -> key.
*/
public class StaticPrecajoleMap implements PrecajoleMap {
private static String SUBDIR_PATH = "com/google/caja/precajole/data/";
private static String INDEX_NAME = "index.dat";
public static StaticPrecajoleMap getInstance() {
return InstanceHolder.instance;
}
private static class InstanceHolder {
static StaticPrecajoleMap instance = new StaticPrecajoleMap("");
}
private static class Entry implements Serializable {
private static final long serialVersionUID = 1L;
final String[] uris;
final String source;
final CajoledModule minified;
final CajoledModule pretty;
final String id;
Entry(String[] uris, String source, CajoledModule cajoled) {
this.uris = uris;
this.source = normalizeSource(source);
this.minified = cajoled.flatten(true);
this.pretty = cajoled.flatten(false);
this.id = idForSource(this.source);
}
static Entry from(byte[] serial) {
return (Entry) deserialize(serial);
}
}
private static class Index implements Serializable {
private static final long serialVersionUID = 1L;
final public Map<String, String> map = Maps.newHashMap();
public long modTime = 0L; // millisecond timestamp
}
//----
private final String dir;
private final Index index;
public StaticPrecajoleMap(File baseDir) {
this(baseDir.toString());
}
public StaticPrecajoleMap(String baseDir) {
if (!baseDir.equals("") && !baseDir.endsWith("/")) {
baseDir += "/";
}
this.dir = baseDir + SUBDIR_PATH;
this.index = readIndex();
}
private Index readIndex() {
Object o = deserialize(load(INDEX_NAME));
if (o != null && o instanceof Index) {
return (Index) o;
} else {
return new Index();
}
}
public void put(List<String> uris, String source, CajoledModule cajoled) {
put(uris.toArray(new String[uris.size()]), source, cajoled);
}
public void put(String[] uris, String source, CajoledModule cajoled) {
Entry entry = new Entry(uris, source, cajoled);
byte[] serial = serialize(entry);
for (int k = 0; k < uris.length; k++) {
index.map.put(normalizeUri(uris[k]), entry.id);
}
save(entry.id, serial);
}
public long getModTime() {
return index.modTime;
}
public void setModTime(long millitime) {
index.modTime = millitime;
}
public void finish() {
save(INDEX_NAME, serialize(index));
}
@Override
public CajoledModule lookupUri(String uri, boolean minify) {
Entry e = Entry.from(load(idForUri(uri)));
if (e != null && e.uris != null) {
for (int k = 0; k < e.uris.length; k++) {
if (uri.equals(e.uris[k])) {
return minify ? e.minified : e.pretty;
}
}
}
return null;
}
@Override
public CajoledModule lookupSource(String source, boolean minify) {
source = normalizeSource(source);
Entry e = Entry.from(load(idForSource(source)));
if (e != null && source.equals(e.source)) {
return minify ? e.minified : e.pretty;
}
return null;
}
public List<List<String>> getUrlGroups() {
Map<String, List<String>> idMap = Maps.newHashMap();
for (String url : index.map.keySet()) {
String id = index.map.get(url);
List<String> urls = idMap.get(id);
if (urls == null) {
urls = Lists.newArrayList();
idMap.put(id, urls);
}
urls.add(url);
}
List<List<String>> urlGroups = Lists.newArrayList();
for (List<String> urls : idMap.values()) {
urlGroups.add(urls);
}
return urlGroups;
}
public static String normalizeUri(String uri) {
try {
URI u = new URI(uri).normalize();
if (u.getHost() != null) {
u = new URI(
lowercase(u.getScheme()),
u.getUserInfo(),
lowercase(u.getHost()),
u.getPort(),
u.getPath(),
u.getQuery(),
u.getFragment());
} else if (u.getScheme() != null) {
u = new URI(
lowercase(u.getScheme()),
u.getSchemeSpecificPart(),
u.getFragment());
}
return u.toString();
} catch (URISyntaxException e) {
return uri;
}
}
private static String lowercase(String s) {
return s == null ? null : Strings.lower(s);
}
private void save(String id, byte[] data) {
try {
new File(dir).mkdirs();
FileOutputStream o = new FileOutputStream(new File(dir, id));
o.write(data);
o.close();
} catch (IOException e) {
throw new SomethingWidgyHappenedError(e);
}
}
private String idForUri(String uri) {
return index.map.get(normalizeUri(uri));
}
private static String idForSource(String source) {
return computeHash(source);
}
private static String normalizeSource(String source) {
// TODO(felix8a): I'd like to minify js here, but minifier is too slow
return source.trim();
}
private byte[] load(String id) {
byte[] result = loadResource(id);
return result != null ? result : loadFile(id);
}
private byte[] loadFile(String id) {
if (id == null) {
return null;
}
try {
return Files.toByteArray(new File(dir, id));
} catch (IOException e) {
return null;
}
}
private byte[] loadResource(String id) {
if (id == null) {
return null;
}
ClassLoader cl = StaticPrecajoleMap.class.getClassLoader();
InputStream is = cl.getResourceAsStream(dir + id);
if (is == null) {
return null;
}
try {
return ByteStreams.toByteArray(is);
} catch (IOException e) {
return null;
}
}
private static String computeHash(String s) {
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
byte[] digest = sha1.digest(s.getBytes("UTF-8"));
return new String(Hex.encodeHex(digest));
} catch (NoSuchAlgorithmException e) {
throw new SomethingWidgyHappenedError(e);
} catch (UnsupportedEncodingException e) {
throw new SomethingWidgyHappenedError(e);
}
}
// TODO(felix8a): protobuf is much faster
private static byte[] serialize(Object obj) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
try {
ObjectOutputStream ostr = new ObjectOutputStream(buf);
ostr.writeObject(obj);
ostr.close();
} catch (IOException e) {
throw new SomethingWidgyHappenedError(e);
}
return buf.toByteArray();
}
private static Object deserialize(byte[] serial) {
if (serial == null) {
return null;
}
try {
ByteArrayInputStream b = new ByteArrayInputStream(serial);
ObjectInputStream i = new ObjectInputStream(b);
return i.readObject();
} catch (IOException e) {
return null;
} catch (ClassNotFoundException e) {
return null;
}
}
}