package org.robolectric.shadows.util; import com.almworks.sqlite4java.SQLite; import com.almworks.sqlite4java.SQLiteException; import org.robolectric.res.Fs; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; /** * Initializes sqlite native libraries. */ public class SQLiteLibraryLoader { private static SQLiteLibraryLoader instance; private static final String SQLITE4JAVA = "sqlite4java"; private static final String OS_WIN = "windows", OS_LINUX = "linux", OS_MAC = "mac"; private final LibraryNameMapper libraryNameMapper; private boolean loaded; public SQLiteLibraryLoader() { this(DEFAULT_MAPPER); } public SQLiteLibraryLoader(LibraryNameMapper mapper) { libraryNameMapper = mapper; } private static final LibraryNameMapper DEFAULT_MAPPER = new LibraryNameMapper() { @Override public String mapLibraryName(String name) { return System.mapLibraryName(name); } }; public static synchronized void load() { if (instance == null) { instance = new SQLiteLibraryLoader(); } instance.doLoad(); } public void doLoad() { if (loaded) { return; } final long startTime = System.currentTimeMillis(); final File extractedLibrary = getNativeLibraryPath(); if (isExtractedLibUptodate(extractedLibrary)) { loadFromDirectory(extractedLibrary.getParentFile()); } else { extractAndLoad(getLibraryStream(), extractedLibrary); } logWithTime("SQLite natives prepared in", startTime); } public File getNativeLibraryPath() { String tempPath = System.getProperty("java.io.tmpdir"); if (tempPath == null) { throw new IllegalStateException("Java temporary directory is not defined (java.io.tmpdir)"); } return new File(Fs.fileFromPath(tempPath).join("robolectric-libs", getLibName()).getPath()); } public void mustReload() { loaded = false; } public String getLibClasspathResourceName() { return "/" + getNativesResourcesPathPart() + "/" + getNativesResourcesFilePart(); } private InputStream getLibraryStream() { final String classpathResourceName = getLibClasspathResourceName(); final InputStream libraryStream = SQLiteLibraryLoader.class.getResourceAsStream(classpathResourceName); if (libraryStream == null) { throw new RuntimeException("Cannot find '" + classpathResourceName + "' in classpath"); } return libraryStream; } private void logWithTime(final String message, final long startTime) { log(message + " " + (System.currentTimeMillis() - startTime)); } private void log(final String message) { org.robolectric.util.Logger.debug(message); } private boolean isExtractedLibUptodate(File extractedLib) { if (extractedLib.exists()) { try { String existingMd5 = md5sum(new FileInputStream(extractedLib)); String actualMd5 = md5sum(getLibraryStream()); return existingMd5.equals(actualMd5); } catch (IOException e) { return false; } } else { return false; } } private void extractAndLoad(final InputStream input, final File output) { File libPath = output.getParentFile(); if (!libPath.exists() && !libPath.mkdirs()) { throw new RuntimeException("could not create " + libPath); } FileOutputStream outputStream = null; try { outputStream = new FileOutputStream(output); copy(input, outputStream); } catch (IOException e) { throw new RuntimeException("Cannot extractAndLoad SQLite library into " + output, e); } finally { closeQuietly(outputStream); closeQuietly(input); } loadFromDirectory(libPath); } private void loadFromDirectory(final File libPath) { // configure less verbose logging Logger.getLogger("com.almworks.sqlite4java").setLevel(Level.WARNING); SQLite.setLibraryPath(libPath.getAbsolutePath()); try { log("SQLite version: library " + SQLite.getLibraryVersion() + " / core " + SQLite.getSQLiteVersion()); } catch (SQLiteException e) { throw new RuntimeException(e); } loaded = true; } private String getLibName() { return libraryNameMapper.mapLibraryName(SQLITE4JAVA); } private String getNativesResourcesPathPart() { return getOsPrefix() + "-" + getArchitectureSuffix(); } private String getNativesResourcesFilePart() { return getLibName().replace(".dylib", ".jnilib"); } private String getOsPrefix() { String name = System.getProperty("os.name").toLowerCase(Locale.US); if (name.contains("win")) { return OS_WIN; } else if (name.contains("linux")) { return OS_LINUX; } else if (name.contains("mac")) { return OS_MAC; } else { throw new UnsupportedOperationException("Architecture '" + name + "' is not supported by SQLite library"); } } private String getArchitectureSuffix() { String arch = System.getProperty("os.arch").toLowerCase(Locale.US).replaceAll("\\W", ""); if ("i386".equals(arch) || "x86".equals(arch)) { return "x86"; } else { return "x86_64"; } } private String md5sum(InputStream input) throws IOException { BufferedInputStream in = new BufferedInputStream(input); try { MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); DigestInputStream digestInputStream = new DigestInputStream(in, digest); while (digestInputStream.read() >= 0) ; ByteArrayOutputStream md5out = new ByteArrayOutputStream(); md5out.write(digest.digest()); return new BigInteger(md5out.toByteArray()).toString(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("MD5 algorithm is not available: " + e); } finally { in.close(); } } public static void copy(final InputStream input, final OutputStream output) throws IOException { byte[] buffer = new byte[4096]; int n; while ((n = input.read(buffer)) != -1) { output.write(buffer, 0, n); } } private static void closeQuietly(final Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { // ignore } } } public interface LibraryNameMapper { String mapLibraryName(String name); } }