replaced json rest server with offline sqlite
This commit is contained in:
parent
75576de7a1
commit
0ee6baa776
21 changed files with 1058 additions and 592 deletions
|
|
@ -1,42 +1,63 @@
|
||||||
package perfix;
|
package perfix;
|
||||||
|
|
||||||
|
import sqlighter.data.Value;
|
||||||
import perfix.instrument.Instrumentor;
|
import perfix.instrument.Instrumentor;
|
||||||
import perfix.server.HTTPServer;
|
import sqlighter.DatabaseBuilder;
|
||||||
|
import sqlighter.data.Record;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.instrument.Instrumentation;
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.LongAdder;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Arrays.stream;
|
|
||||||
|
|
||||||
public class Agent {
|
public class Agent {
|
||||||
|
|
||||||
private static final String PORT_PROPERTY = "perfix.port";
|
|
||||||
private static final String INCLUDES_PROPERTY = "perfix.includes";
|
private static final String INCLUDES_PROPERTY = "perfix.includes";
|
||||||
|
|
||||||
private static final String DEFAULT_PORT = "2048";
|
|
||||||
private static final String MESSAGE = " --- Perfix agent active";
|
private static final String MESSAGE = " --- Perfix agent active";
|
||||||
|
|
||||||
|
private static final DatabaseBuilder databaseBuilder = new DatabaseBuilder();
|
||||||
|
|
||||||
public static void premain(String agentArgs, Instrumentation instrumentation) {
|
public static void premain(String agentArgs, Instrumentation instrumentation) {
|
||||||
System.out.println(MESSAGE);
|
System.out.println(MESSAGE);
|
||||||
|
|
||||||
int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
|
|
||||||
|
|
||||||
Instrumentor.create(determineIncludes()).instrumentCode(instrumentation);
|
Instrumentor.create(determineIncludes()).instrumentCode(instrumentation);
|
||||||
|
System.out.println("Instrumenting " + System.getProperty(INCLUDES_PROPERTY));
|
||||||
|
|
||||||
new HTTPServer(port).start();
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
LongAdder rowid = new LongAdder();
|
||||||
|
try {
|
||||||
|
Registry.sortedMethodsByDuration().values()
|
||||||
|
.forEach(r -> {
|
||||||
|
rowid.increment();
|
||||||
|
Record record = new Record(rowid.intValue());
|
||||||
|
record.addValues(Value.of(r.getName()), Value.of(r.getInvocations()), Value.of(r.getAverage() / 1_000_000F), Value.of(r.getTotalDuration() / 1_000_000F));
|
||||||
|
databaseBuilder.addRecord(record);
|
||||||
|
});
|
||||||
|
databaseBuilder.addSchema("results", "create table results(name varchar(100), invocations integer, average float, total float)");
|
||||||
|
databaseBuilder.build().write(Files.newByteChannel(Paths.get("results.sqlite"), StandardOpenOption.WRITE, StandardOpenOption.CREATE));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> determineIncludes() {
|
private static List<String> determineIncludes() {
|
||||||
String includesPropertyValue = System.getProperty(INCLUDES_PROPERTY);
|
String includesPropertyValue = System.getProperty(INCLUDES_PROPERTY).replaceAll("\\.", "/");
|
||||||
if (includesPropertyValue==null){
|
if (includesPropertyValue == null) {
|
||||||
System.out.println("WARNING: perfix.includes not set ");
|
System.out.println("WARNING: perfix.includes not set ");
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
} else {
|
||||||
return new ArrayList<>(asList(includesPropertyValue.split(",")));
|
ArrayList<String> includes = new ArrayList<>(asList(includesPropertyValue.split(",")));
|
||||||
|
System.out.println("includes classes: " + includes + "*");
|
||||||
|
return includes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,20 +50,21 @@ public class ClassInstrumentor extends Instrumentor {
|
||||||
return servletInstrumentor.instrumentServlet(ctClass, uninstrumentedByteCode);
|
return servletInstrumentor.instrumentServlet(ctClass, uninstrumentedByteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jdbcInstrumentor.isJdbcStatementImpl(resource, ctClass)) {
|
// if (jdbcInstrumentor.isJdbcStatementImpl(resource, ctClass)) {
|
||||||
return jdbcInstrumentor.instrumentJdbcStatement(ctClass, uninstrumentedByteCode);
|
// return jdbcInstrumentor.instrumentJdbcStatement(ctClass, uninstrumentedByteCode);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
// if (jdbcInstrumentor.isJdbcConnectionImpl(resource, ctClass)) {
|
||||||
|
// return jdbcInstrumentor.instrumentJdbcConnection(ctClass, uninstrumentedByteCode);
|
||||||
|
// }
|
||||||
|
|
||||||
if (jdbcInstrumentor.isJdbcConnectionImpl(resource, ctClass)) {
|
// if (jdbcInstrumentor.isJdbcPreparedStatement(resource)) {
|
||||||
return jdbcInstrumentor.instrumentJdbcConnection(ctClass, uninstrumentedByteCode);
|
// return jdbcInstrumentor.instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode);
|
||||||
}
|
// }
|
||||||
|
// if (jdbcInstrumentor.isJdbcPreparedStatementImpl(resource, ctClass)) {
|
||||||
if (jdbcInstrumentor.isJdbcPreparedStatement(resource)) {
|
// return jdbcInstrumentor.instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode);
|
||||||
return jdbcInstrumentor.instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode);
|
// }
|
||||||
}
|
// System.out.println(resource);
|
||||||
if (jdbcInstrumentor.isJdbcPreparedStatementImpl(resource, ctClass)) {
|
|
||||||
return jdbcInstrumentor.instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode);
|
|
||||||
}
|
|
||||||
if (shouldInclude(resource, includes)) {
|
if (shouldInclude(resource, includes)) {
|
||||||
return instrumentMethods(ctClass, uninstrumentedByteCode);
|
return instrumentMethods(ctClass, uninstrumentedByteCode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
package perfix.server;
|
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
|
||||||
import perfix.server.json.Serializer;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
public class HTTPServer implements HttpHandler {
|
|
||||||
|
|
||||||
private static final String DEFAULT_ROUTE = "DEFAULT";
|
|
||||||
private final int port;
|
|
||||||
private final ConcurrentMap<String, Function<HttpExchange, ?>> routes = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public HTTPServer(int port) {
|
|
||||||
this.port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
try {
|
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
|
|
||||||
server.createContext("/", this);
|
|
||||||
server.setExecutor(Executors.newFixedThreadPool(3));
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
PerfixController perfixController = new PerfixController();
|
|
||||||
routes.put("/report", perfixController::perfixMetrics);
|
|
||||||
routes.put("/callstack", perfixController::perfixCallstack);
|
|
||||||
routes.put("/clear", perfixController::clear);
|
|
||||||
routes.put(DEFAULT_ROUTE, this::staticContent);
|
|
||||||
|
|
||||||
System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + port + "/");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
System.err.println(" --- Couldn't start Perfix http server:\n" + ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(HttpExchange exchange) throws IOException {
|
|
||||||
InputStream response = getResponse(exchange);
|
|
||||||
|
|
||||||
OutputStream outputStream = exchange.getResponseBody();
|
|
||||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
|
||||||
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
||||||
int length = response.available();
|
|
||||||
exchange.sendResponseHeaders(200, length);
|
|
||||||
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
outputStream.write(response.read());
|
|
||||||
}
|
|
||||||
outputStream.flush();
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private InputStream getResponse(HttpExchange exchange) {
|
|
||||||
String uri = exchange.getRequestURI().toString();
|
|
||||||
Object response;
|
|
||||||
if (routes.get(uri) != null) {
|
|
||||||
response = routes.get(uri).apply(exchange);
|
|
||||||
} else {
|
|
||||||
response = routes.get(DEFAULT_ROUTE).apply(exchange);
|
|
||||||
}
|
|
||||||
if (response instanceof InputStream) {
|
|
||||||
return (InputStream) response;
|
|
||||||
} else {
|
|
||||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
|
||||||
return toStream(Serializer.toJSONString(response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InputStream staticContent(HttpExchange exchange) {
|
|
||||||
String uri = exchange.getRequestURI().toString();
|
|
||||||
if (uri.equals("/")) {
|
|
||||||
uri = "/index.html";
|
|
||||||
}
|
|
||||||
InputStream resource = getClass().getResourceAsStream(uri);
|
|
||||||
if (resource != null) {
|
|
||||||
String mimeType;
|
|
||||||
if (uri.endsWith("css")) {
|
|
||||||
mimeType = "text/css";
|
|
||||||
} else if (uri.endsWith("js")) {
|
|
||||||
mimeType = "application/ecmascript";
|
|
||||||
} else if (uri.equals("/favicon.ico")) {
|
|
||||||
mimeType = "image/svg+xml";
|
|
||||||
} else {
|
|
||||||
mimeType = "text/html";
|
|
||||||
}
|
|
||||||
exchange.getResponseHeaders().add("Content-Type", mimeType);
|
|
||||||
return resource;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return toStream(notFound());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String notFound() {
|
|
||||||
return "NOT FOUND";
|
|
||||||
}
|
|
||||||
|
|
||||||
private InputStream toStream(String text) {
|
|
||||||
return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package perfix.server;
|
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import perfix.MethodNode;
|
|
||||||
import perfix.Registry;
|
|
||||||
import perfix.Report;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class PerfixController {
|
|
||||||
|
|
||||||
public List<Report> perfixMetrics(HttpExchange exchange) {
|
|
||||||
return new ArrayList<>(Registry.sortedMethodsByDuration().values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MethodNode> perfixCallstack(HttpExchange exchange) {
|
|
||||||
return Registry.getCallStack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String clear(HttpExchange exchange) {
|
|
||||||
Registry.clear();
|
|
||||||
return "clear";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package perfix.server.json;
|
|
||||||
|
|
||||||
import java.util.Formatter;
|
|
||||||
|
|
||||||
public abstract class JSONSerializer<T> {
|
|
||||||
protected abstract String handle(T object);
|
|
||||||
|
|
||||||
protected Formatter formatter = new Formatter();
|
|
||||||
|
|
||||||
public String toJSONString(T object) {
|
|
||||||
if (object == null) {
|
|
||||||
return "";
|
|
||||||
} else if (object instanceof Number || object instanceof Boolean) {
|
|
||||||
return "" + object.toString();
|
|
||||||
} else if (object instanceof CharSequence || object instanceof Character) {
|
|
||||||
return "\"" + object.toString() + "\"";
|
|
||||||
} else {
|
|
||||||
return handle(object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
package perfix.server.json;
|
|
||||||
|
|
||||||
public class Serializer {
|
|
||||||
private static SerializerFactory instance = new SynthSerializerFactory();
|
|
||||||
|
|
||||||
public static String toJSONString(boolean b) {
|
|
||||||
return Boolean.toString(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(short s) {
|
|
||||||
return Short.toString(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(int i) {
|
|
||||||
return Integer.toString(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(float f) {
|
|
||||||
return Float.toString(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(double d) {
|
|
||||||
return Double.toString(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(long l) {
|
|
||||||
return Long.toString(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(char c) {
|
|
||||||
return "\"" + Character.toString(c) + "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> String toJSONString(T o) {
|
|
||||||
if (o == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
return instance.createSerializer((Class<T>) o.getClass()).toJSONString(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setInstance(SerializerFactory instance) {
|
|
||||||
Serializer.instance = instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package perfix.server.json;
|
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
public class SerializerCreationException extends RuntimeException {
|
|
||||||
|
|
||||||
public SerializerCreationException(Throwable t) {
|
|
||||||
super(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package perfix.server.json;
|
|
||||||
|
|
||||||
public interface SerializerFactory {
|
|
||||||
<T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass);
|
|
||||||
}
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
package perfix.server.json;
|
|
||||||
|
|
||||||
import javassist.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
import static java.util.Collections.unmodifiableSet;
|
|
||||||
|
|
||||||
public class SynthSerializerFactory implements SerializerFactory {
|
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger("perfix");
|
|
||||||
private static final String STRING = "java.lang.String";
|
|
||||||
private static final String BOOLEAN = "java.lang.Boolean";
|
|
||||||
private static final String CHARACTER = "java.lang.Character";
|
|
||||||
private static final String BYTE = "java.lang.Byte";
|
|
||||||
private static final String DOUBLE = "java.lang.Double";
|
|
||||||
private static final String FLOAT = "java.lang.Float";
|
|
||||||
private static final String LONG = "java.lang.Long";
|
|
||||||
private static final String SHORT = "java.lang.Short";
|
|
||||||
private static final String INTEGER = "java.lang.Integer";
|
|
||||||
|
|
||||||
private final static Set<String> wrappersAndString = unmodifiableSet(new HashSet<String>(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
|
|
||||||
STRING)));
|
|
||||||
|
|
||||||
private static final String COLLECTION = "java.util.Collection";
|
|
||||||
private static final String LIST = "java.util.List";
|
|
||||||
private static final String SET = "java.util.Set";
|
|
||||||
private static final List<String> mapInterfaces = Collections.unmodifiableList(asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap"));
|
|
||||||
|
|
||||||
private static final ConcurrentMap<String, JSONSerializer<?>> serializers = new ConcurrentHashMap<>();
|
|
||||||
private static final String ROOT_PACKAGE = "serializer.";
|
|
||||||
|
|
||||||
private final ClassPool pool = ClassPool.getDefault();
|
|
||||||
private CtClass serializerBase;
|
|
||||||
|
|
||||||
SynthSerializerFactory() {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) {
|
|
||||||
return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
void init() {
|
|
||||||
try {
|
|
||||||
serializerBase = pool.get(JSONSerializer.class.getName());
|
|
||||||
} catch (NotFoundException e) {
|
|
||||||
throw new SerializerCreationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
|
|
||||||
String serializerName = createSerializerName(beanjavaClass);
|
|
||||||
return (JSONSerializer<T>) serializers.computeIfAbsent(serializerName, key -> {
|
|
||||||
try {
|
|
||||||
CtClass beanClass = pool.get(beanjavaClass.getName());
|
|
||||||
CtClass serializerClass = pool.makeClass(serializerName, serializerBase);
|
|
||||||
|
|
||||||
addToJsonStringMethod(beanClass, serializerClass);
|
|
||||||
|
|
||||||
return createSerializerInstance(serializerClass);
|
|
||||||
|
|
||||||
} catch (NotFoundException | CannotCompileException | ReflectiveOperationException e) {
|
|
||||||
log.severe(e.toString());
|
|
||||||
throw new SerializerCreationException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* create method source, compile it and add it to the class under construction
|
|
||||||
*/
|
|
||||||
private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws
|
|
||||||
NotFoundException, CannotCompileException {
|
|
||||||
String body = createToJSONStringMethodSource(beanClass);
|
|
||||||
serializerClass.addMethod(CtNewMethod.make(body, serializerClass));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Creates the source, handling the for JSON different types of classes
|
|
||||||
*/
|
|
||||||
private String createToJSONStringMethodSource(CtClass beanClass) throws NotFoundException {
|
|
||||||
String source = "public String handle(Object object){\n";
|
|
||||||
if (beanClass.isArray()) {
|
|
||||||
source += "\tObject[] array=(Object[])object;\n";
|
|
||||||
source += handleArray(beanClass);
|
|
||||||
} else if (isCollection(beanClass)) {
|
|
||||||
source += "\tObject[] array=((java.util.Collection)object).toArray();\n";
|
|
||||||
source += handleArray(beanClass);
|
|
||||||
} else if (isMap(beanClass)) {
|
|
||||||
source += handleMap(beanClass);
|
|
||||||
} else if (!isPrimitiveOrWrapperOrString(beanClass)) {
|
|
||||||
List<CtMethod> getters = getGetters(beanClass);
|
|
||||||
if (shouldAddGetterCallers(getters)) {
|
|
||||||
source = addGetterCallers(beanClass, source, getters);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
source += "\treturn \"\";}";
|
|
||||||
}
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Any Collection is converted to an array, after which code is generated to handle the single elements.
|
|
||||||
*
|
|
||||||
* A subserializer is created for every single element, but most of the time it will be the same cached instance.
|
|
||||||
*
|
|
||||||
* The generated code fills a StringBuilder. The values are generated by the subserializers
|
|
||||||
*/
|
|
||||||
private String handleArray(CtClass beanClass) {
|
|
||||||
String source = "\tjava.util.StringJoiner result=new java.util.StringJoiner(\",\",\"[\",\"]\");\n";
|
|
||||||
source += "\tfor (int i=0; i<array.length; i++){\n";
|
|
||||||
source += "\t\tresult.add(" + Serializer.class.getName() + ".toJSONString(array[i]));\n";
|
|
||||||
source += "\t};\n\treturn result.toString();\n}";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String handleMap(CtClass beanClass) {
|
|
||||||
String source = "StringBuilder result=new StringBuilder(\"{\");\n";
|
|
||||||
source += "\tfor (java.util.Iterator entries=((java.util.Map)object).entrySet().iterator();entries.hasNext();){\n";
|
|
||||||
source += "\t\tjava.util.Map.Entry entry=(java.util.Map.Entry)entries.next();\n";
|
|
||||||
source += "\t\tresult.append(\"\\\"\"+entry.getKey().toString()+\"\\\"\");\n";
|
|
||||||
source += "\t\tresult.append(\": \");\n";
|
|
||||||
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(entry.getValue()));\n";
|
|
||||||
source += "\t\tresult.append(\", \");\n";
|
|
||||||
source += "\t};\n";
|
|
||||||
source += "\tresult.setLength(result.length()-2);\n";
|
|
||||||
source += "\tresult.append(\"}\");\n";
|
|
||||||
source += "\treturn result.toString();\n";
|
|
||||||
source += "}";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If the class contains fields for which public getters are available, then these will be called in the generated code.
|
|
||||||
*/
|
|
||||||
private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> getters) throws
|
|
||||||
NotFoundException {
|
|
||||||
int index = 0;
|
|
||||||
source += "\treturn ";
|
|
||||||
source += "\"{";
|
|
||||||
for (CtMethod getter : getters) {
|
|
||||||
source = addPair(beanClass, source, getter);
|
|
||||||
if (index++ < getters.size() - 1) {
|
|
||||||
source += ",";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
source += "}\";\n}";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws
|
|
||||||
CannotCompileException, ReflectiveOperationException {
|
|
||||||
return (JSONSerializer<T>) pool.toClass(serializerClass).getConstructor().newInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes
|
|
||||||
*
|
|
||||||
* Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
|
|
||||||
*/
|
|
||||||
public String createSerializerName(Class<?> beanClass) {
|
|
||||||
return createSerializerName(beanClass.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String createSerializerName(String name) {
|
|
||||||
return ROOT_PACKAGE + name.replaceAll("\\[]", "Array") + "Serializer";
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCollection(CtClass beanClass) throws NotFoundException {
|
|
||||||
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
|
|
||||||
interfaces.add(beanClass);
|
|
||||||
return interfaces.stream()
|
|
||||||
.map(CtClass::getName)
|
|
||||||
.anyMatch(interfaze -> interfaze.equals(COLLECTION) || interfaze.equals(LIST) || interfaze.equals(SET));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isMap(CtClass beanClass) throws NotFoundException {
|
|
||||||
if (mapInterfaces.contains(beanClass.getName())) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return Arrays.stream(beanClass.getInterfaces())
|
|
||||||
.map(CtClass::getName)
|
|
||||||
.anyMatch(mapInterfaces::contains);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The JSON vernacular for key:value is pair...
|
|
||||||
*/
|
|
||||||
private String addPair(CtClass classToSerialize, String source, CtMethod getter) throws NotFoundException {
|
|
||||||
source += jsonKey(getter);
|
|
||||||
source += ":";
|
|
||||||
source += jsonValue(classToSerialize, getter);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* derive property key from getter
|
|
||||||
*/
|
|
||||||
private String jsonKey(CtMethod getter) {
|
|
||||||
return "\\\"" + toFieldName(getter.getName()) + "\\\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String jsonValue(CtClass classToSerialize, CtMethod getter) throws NotFoundException {
|
|
||||||
String source = "";
|
|
||||||
CtClass returnType = getter.getReturnType();
|
|
||||||
|
|
||||||
/* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */
|
|
||||||
source = createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter, source, returnType);
|
|
||||||
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod
|
|
||||||
getter, String source, CtClass returnType) {
|
|
||||||
/* NB there does not seem to be auto(un))boxing nor generic types (or other jdk1.5 stuff) in javassist compileable code */
|
|
||||||
|
|
||||||
source += "\"+" + Serializer.class.getName() + ".toJSONString(";
|
|
||||||
|
|
||||||
// cast because of lack of generics
|
|
||||||
source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()";
|
|
||||||
|
|
||||||
source += ")+\"";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* turns for example 'getValue' into 'value'
|
|
||||||
*/
|
|
||||||
private String toFieldName(String name) {
|
|
||||||
return name.substring(3, 4).toLowerCase() + (name.length() > 4 ? name.substring(4) : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String regularClassname(String name) {
|
|
||||||
return name.replaceAll("\\$", ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String cast(String classToSerialize) {
|
|
||||||
return "(" + classToSerialize + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Retrieves getter methods from a class
|
|
||||||
*/
|
|
||||||
private List<CtMethod> getGetters(CtClass beanClass) {
|
|
||||||
List<CtMethod> methods = new ArrayList<CtMethod>();
|
|
||||||
List<CtField> fields = getAllFields(beanClass);
|
|
||||||
for (CtField field : fields) {
|
|
||||||
try {
|
|
||||||
CtMethod method = beanClass.getMethod(getGetterMethod(field), getDescription(field));
|
|
||||||
if (Modifier.isPublic(method.getModifiers())) {
|
|
||||||
methods.add(method);
|
|
||||||
}
|
|
||||||
} catch (NotFoundException n) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return methods;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getGetterMethod(CtField field) {
|
|
||||||
return "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CtField> getAllFields(CtClass beanClass) {
|
|
||||||
try {
|
|
||||||
List<CtField> allfields = new ArrayList<>();
|
|
||||||
return getAllFields(beanClass, allfields);
|
|
||||||
} catch (NotFoundException e) {
|
|
||||||
throw new SerializerCreationException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CtField> getAllFields(CtClass beanClass, List<CtField> allfields) throws NotFoundException {
|
|
||||||
allfields.addAll(asList(beanClass.getDeclaredFields()));
|
|
||||||
if (beanClass.getSuperclass() != null) {
|
|
||||||
return getAllFields(beanClass.getSuperclass(), allfields);
|
|
||||||
}
|
|
||||||
return allfields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* is getter list is not empty then callers should be added
|
|
||||||
*/
|
|
||||||
boolean shouldAddGetterCallers(List<CtMethod> getters) {
|
|
||||||
return !getters.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
String getDescription(CtField field) throws NotFoundException {
|
|
||||||
if (field.getType().isArray()) {
|
|
||||||
return "()[" + innerClassName(field.getType().getName()) + ";";
|
|
||||||
} else if (!field.getType().isPrimitive()) {
|
|
||||||
return "()" + innerClassName(field.getType().getName()) + ";";
|
|
||||||
} else {
|
|
||||||
|
|
||||||
return "()" + asPrimitive(field.getType().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String asPrimitive(String name) {
|
|
||||||
switch (name) {
|
|
||||||
case "int":
|
|
||||||
return "I";
|
|
||||||
case "byte":
|
|
||||||
return "B";
|
|
||||||
case "float":
|
|
||||||
return "F";
|
|
||||||
case "long":
|
|
||||||
return "J";
|
|
||||||
case "boolean":
|
|
||||||
return "Z";
|
|
||||||
case "char":
|
|
||||||
return "C";
|
|
||||||
case "double":
|
|
||||||
return "D";
|
|
||||||
case "short":
|
|
||||||
return "S";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String innerClassName(String name) {
|
|
||||||
return "L" + name.replaceAll("\\.", "/").replaceAll("\\[\\]", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
216
src/main/java/sqlighter/Database.java
Normal file
216
src/main/java/sqlighter/Database.java
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
package sqlighter;
|
||||||
|
|
||||||
|
import sqlighter.page.Page;
|
||||||
|
import sqlighter.page.PageCacheFactory;
|
||||||
|
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static sqlighter.SQLiteConstants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limited to one table. As this is the main use case, this will probably not change.
|
||||||
|
* Please note that you can put whatever in it (does not have to reflect the actual source database structure),
|
||||||
|
* including for example the result of a complex join
|
||||||
|
* major #1 TODO find a way to handle sizes that don't fit into memory, like overflow to file or sth
|
||||||
|
*/
|
||||||
|
public class Database {
|
||||||
|
|
||||||
|
public static short PAGE_SIZE = 8192;
|
||||||
|
|
||||||
|
private final SchemaRecord schema;
|
||||||
|
public final int pageSize;
|
||||||
|
|
||||||
|
final List<Page> leafPages;
|
||||||
|
|
||||||
|
private int pageCounter = 3;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* assumes 1 schema record ie 1 table. This might not change
|
||||||
|
*/
|
||||||
|
public Database(int pageSize, SchemaRecord schemaRecord, List<Page> leafPages) {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
this.schema = schemaRecord;
|
||||||
|
this.leafPages = leafPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(String filename) throws IOException {
|
||||||
|
try (FileOutputStream outputStream = new FileOutputStream(filename)) {
|
||||||
|
write(outputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(WritableByteChannel channel) throws IOException {
|
||||||
|
List<? extends Page> currentTopLayer = this.leafPages;
|
||||||
|
int nPages = currentTopLayer.size();
|
||||||
|
while (currentTopLayer.size() > 1) { // interior page needed?
|
||||||
|
currentTopLayer = createInteriorPages(currentTopLayer);
|
||||||
|
nPages += currentTopLayer.size();
|
||||||
|
}
|
||||||
|
assert !currentTopLayer.isEmpty();
|
||||||
|
Page tableRootPage = currentTopLayer.get(0); //
|
||||||
|
channel.write(createHeaderPage(nPages + 1).getDataBuffer());
|
||||||
|
setChildReferencesAndWrite(tableRootPage, channel);
|
||||||
|
channel.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(OutputStream outputStream) throws IOException {
|
||||||
|
List<? extends Page> currentTopLayer = this.leafPages;
|
||||||
|
int nPages = currentTopLayer.size();
|
||||||
|
while (currentTopLayer.size() > 1) { // interior page needed?
|
||||||
|
currentTopLayer = createInteriorPages(currentTopLayer);
|
||||||
|
nPages += currentTopLayer.size();
|
||||||
|
}
|
||||||
|
assert !currentTopLayer.isEmpty();
|
||||||
|
Page tableRootPage = currentTopLayer.get(0); //
|
||||||
|
outputStream.write(createHeaderPage(nPages + 1).getData());
|
||||||
|
setChildReferencesAndWrite(tableRootPage, outputStream);
|
||||||
|
outputStream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setChildReferencesAndWrite(Page page, OutputStream outputStream) {
|
||||||
|
if (page.isInterior()) {
|
||||||
|
setChildReferences(page);
|
||||||
|
}
|
||||||
|
write(page, outputStream);
|
||||||
|
PageCacheFactory.getPageCache().release(page);
|
||||||
|
//recurse
|
||||||
|
page.getChildren().forEach(child -> setChildReferencesAndWrite(child, outputStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setChildReferencesAndWrite(Page page, WritableByteChannel channel) {
|
||||||
|
if (page.isInterior()) {
|
||||||
|
setChildReferences(page);
|
||||||
|
}
|
||||||
|
write(page, channel);
|
||||||
|
PageCacheFactory.getPageCache().release(page);
|
||||||
|
//recurse
|
||||||
|
page.getChildren().forEach(child -> setChildReferencesAndWrite(child, channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(Page page, OutputStream outputStream) {
|
||||||
|
try {
|
||||||
|
outputStream.write(page.getData());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(Page page, WritableByteChannel channel) {
|
||||||
|
try {
|
||||||
|
channel.write(page.getDataBuffer());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setChildReferences(Page page) {
|
||||||
|
page.setForwardPosition(Page.POSITION_CELL_COUNT);
|
||||||
|
page.putU16(page.getChildren().size() - 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < page.getChildren().size() - 1; i++) { // except right-most pointer
|
||||||
|
page.setForwardPosition(Page.START + i * 2);
|
||||||
|
int position = page.getU16(); // read the position that was written in an earlier pass
|
||||||
|
page.setForwardPosition(position); // go to the cell at that location
|
||||||
|
page.putU32(pageCounter++); // add page reference
|
||||||
|
}
|
||||||
|
page.setForwardPosition(Page.POSITION_RIGHTMOST_POINTER);
|
||||||
|
page.putU32(pageCounter++);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Page createHeaderPage(int nPages) {
|
||||||
|
Page headerPage = Page.newHeader(pageSize);
|
||||||
|
writeHeader(headerPage, nPages);
|
||||||
|
int payloadLocationWriteLocation = headerPage.getForwardPosition(); // mark current position
|
||||||
|
|
||||||
|
int payloadLocation = writeSchema(headerPage, schema); //write schema payload from the end
|
||||||
|
headerPage.setForwardPosition(payloadLocationWriteLocation); // go back to marked position
|
||||||
|
headerPage.putU16(payloadLocation); //payload start
|
||||||
|
headerPage.skipForward(1); // the number of fragmented free bytes within the cell content area
|
||||||
|
headerPage.putU16(payloadLocation); // first cell
|
||||||
|
return headerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int writeSchema(Page rootPage, SchemaRecord schemaRecord) {
|
||||||
|
rootPage.putBackward(schemaRecord.toRecord().toBytes());
|
||||||
|
return rootPage.getBackwardPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Page> createInteriorPages(List<? extends Page> childPages) {
|
||||||
|
List<Page> interiorPages = new ArrayList<>();
|
||||||
|
Page interiorPage = PageCacheFactory.getPageCache().getInteriorPage();
|
||||||
|
interiorPage.setKey(childPages.stream().mapToLong(Page::getKey).max().orElse(-1));
|
||||||
|
interiorPage.setForwardPosition(Page.START);
|
||||||
|
int pageIndex;
|
||||||
|
for (pageIndex = 0; pageIndex < childPages.size() - 1; pageIndex++) {
|
||||||
|
Page leafPage = childPages.get(pageIndex);
|
||||||
|
if (interiorPage.getBackwardPosition() < interiorPage.getForwardPosition() + 10) {
|
||||||
|
interiorPage.setForwardPosition(Page.START_OF_CONTENT_AREA);
|
||||||
|
interiorPage.putU16(interiorPage.getBackwardPosition());
|
||||||
|
interiorPage.skipForward(5);
|
||||||
|
|
||||||
|
interiorPages.add(interiorPage);
|
||||||
|
|
||||||
|
interiorPage = PageCacheFactory.getPageCache().getInteriorPage();
|
||||||
|
interiorPage.setForwardPosition(Page.START);
|
||||||
|
}
|
||||||
|
addCellWithPageRef(interiorPage, leafPage);
|
||||||
|
interiorPage.addChild(leafPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write start of payload
|
||||||
|
interiorPage.setForwardPosition(Page.START_OF_CONTENT_AREA);
|
||||||
|
interiorPage.putU16(interiorPage.getBackwardPosition());
|
||||||
|
interiorPage.skipForward(5);
|
||||||
|
interiorPage.addChild(childPages.get(pageIndex));
|
||||||
|
interiorPages.add(interiorPage);
|
||||||
|
return interiorPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCellWithPageRef(Page interiorPage, Page leafPage) {
|
||||||
|
byte[] keyAsBytes = Varint.write(leafPage.getKey());
|
||||||
|
ByteBuffer cell = ByteBuffer.allocate(6 + keyAsBytes.length);
|
||||||
|
cell.position(5);
|
||||||
|
cell.put(keyAsBytes);
|
||||||
|
|
||||||
|
// write cell to page, starting at the end
|
||||||
|
interiorPage.putBackward(cell.array());
|
||||||
|
interiorPage.putU16(interiorPage.getBackwardPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeHeader(Page rootpage, int nPages) {
|
||||||
|
rootpage.putU8(MAGIC_HEADER);
|
||||||
|
rootpage.putU16(rootpage.size());
|
||||||
|
rootpage.putU8(FILE_FORMAT_WRITE_VERSION);
|
||||||
|
rootpage.putU8(FILE_FORMAT_READ_VERSION);
|
||||||
|
rootpage.putU8(RESERVED_SIZE);
|
||||||
|
rootpage.putU8(MAX_EMBED_PAYLOAD_FRACTION);
|
||||||
|
rootpage.putU8(MIN_EMBED_PAYLOAD_FRACTION);
|
||||||
|
rootpage.putU8(LEAF_PAYLOAD_FRACTION);
|
||||||
|
rootpage.putU32(FILECHANGE_COUNTER);
|
||||||
|
rootpage.putU32(nPages);// file size in pages
|
||||||
|
rootpage.putU32(FREELIST_TRUNK_PAGE_HUMBER);// Page number of the first freelist trunk page.
|
||||||
|
rootpage.putU32(TOTAL_N_FREELIST_PAGES);
|
||||||
|
rootpage.putU32(SCHEMA_COOKIE);
|
||||||
|
rootpage.putU32(SQLITE_SCHEMAVERSION);
|
||||||
|
rootpage.putU32(SUGGESTED_CACHESIZE);
|
||||||
|
rootpage.putU32(LARGEST_ROOT_BTREE_PAGE);
|
||||||
|
rootpage.putU32(ENCODING_UTF8);
|
||||||
|
rootpage.putU32(USER_VERSION);
|
||||||
|
rootpage.putU32(VACUUM_MODE_OFF);// True (non-zero) for incremental-vacuum mode. False (zero) otherwise.
|
||||||
|
rootpage.putU32(APP_ID);// Application ID
|
||||||
|
rootpage.putU8(FILLER);// Reserved for expansion. Must be zero.
|
||||||
|
rootpage.putU8(VERSION_VALID_FOR);// The version-valid-for number
|
||||||
|
rootpage.putU8(SQLITE_VERSION);// SQLITE_VERSION_NUMBER
|
||||||
|
rootpage.putU8(TABLE_LEAF_PAGE); // leaf table b-tree page for schema
|
||||||
|
rootpage.putU16(NO_FREE_BLOCKS); // zero if there are no freeblocks
|
||||||
|
rootpage.putU16(1); // the number of cells on the page
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
src/main/java/sqlighter/DatabaseBuilder.java
Normal file
76
src/main/java/sqlighter/DatabaseBuilder.java
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package sqlighter;
|
||||||
|
|
||||||
|
import sqlighter.data.Record;
|
||||||
|
import sqlighter.page.Page;
|
||||||
|
import sqlighter.page.PageCacheFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database builder is the main interface to create a database.
|
||||||
|
*/
|
||||||
|
public class DatabaseBuilder {
|
||||||
|
|
||||||
|
private int pageSize = Database.PAGE_SIZE;
|
||||||
|
private final List<Page> leafPages = new ArrayList<>();
|
||||||
|
private Page currentPage;
|
||||||
|
|
||||||
|
private SchemaRecord schemaRecord;
|
||||||
|
|
||||||
|
private int nRecordsOnCurrentPage;
|
||||||
|
|
||||||
|
public DatabaseBuilder() {
|
||||||
|
createPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseBuilder withPageSize(int pageSize) {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRecord(final Record record) {
|
||||||
|
if (currentPageIsFull(record)) {
|
||||||
|
finishCurrentPage();
|
||||||
|
createPage();
|
||||||
|
}
|
||||||
|
currentPage.setKey(record.getRowId()); //gets updated until page is finished
|
||||||
|
currentPage.putBackward(record.toBytes());
|
||||||
|
currentPage.putU16(currentPage.getBackwardPosition());
|
||||||
|
nRecordsOnCurrentPage += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSchema(String tableName, String ddl) {
|
||||||
|
this.schemaRecord = new SchemaRecord(1, tableName, 2, ddl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Database build() {
|
||||||
|
currentPage.setForwardPosition(Page.POSITION_CELL_COUNT);
|
||||||
|
currentPage.putU16(nRecordsOnCurrentPage);
|
||||||
|
|
||||||
|
if (nRecordsOnCurrentPage > 0) {
|
||||||
|
currentPage.putU16(currentPage.getBackwardPosition());
|
||||||
|
} else {
|
||||||
|
currentPage.putU16(currentPage.getBackwardPosition() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Database(pageSize, schemaRecord, leafPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean currentPageIsFull(Record record) {
|
||||||
|
return currentPage.getBackwardPosition() - record.getDataLength() < currentPage.getForwardPosition() + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishCurrentPage() {
|
||||||
|
currentPage.setForwardPosition(Page.POSITION_CELL_COUNT);
|
||||||
|
currentPage.putU16(nRecordsOnCurrentPage);
|
||||||
|
currentPage.putU16(currentPage.getBackwardPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPage() {
|
||||||
|
currentPage = PageCacheFactory.getPageCache().getLeafPage();
|
||||||
|
currentPage.setForwardPosition(8);
|
||||||
|
leafPages.add(currentPage);
|
||||||
|
nRecordsOnCurrentPage = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/sqlighter/SQLiteConstants.java
Normal file
43
src/main/java/sqlighter/SQLiteConstants.java
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package sqlighter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* special values for SQLite.
|
||||||
|
*
|
||||||
|
* See <a href="https://sqlite.org/fileformat2.html">Database File Format </a>
|
||||||
|
*/
|
||||||
|
public class SQLiteConstants {
|
||||||
|
public static final byte[] MAGIC_HEADER = new byte[]{0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00};
|
||||||
|
|
||||||
|
public static final byte FILE_FORMAT_WRITE_VERSION = 1; // legacy
|
||||||
|
public static final byte FILE_FORMAT_READ_VERSION = 1; // legacy
|
||||||
|
public static final byte RESERVED_SIZE = 0;
|
||||||
|
public static final byte MAX_EMBED_PAYLOAD_FRACTION = 0x40;
|
||||||
|
public static final byte MIN_EMBED_PAYLOAD_FRACTION = 0x20;
|
||||||
|
public static final byte LEAF_PAYLOAD_FRACTION = 0x20;
|
||||||
|
public static final int FILECHANGE_COUNTER = 1;
|
||||||
|
public static final int FREELIST_TRUNK_PAGE_HUMBER = 0;
|
||||||
|
public static final int TOTAL_N_FREELIST_PAGES = 0;
|
||||||
|
public static final int SCHEMA_COOKIE = 1;
|
||||||
|
public static final int SQLITE_SCHEMAVERSION = 4;
|
||||||
|
public static final int SUGGESTED_CACHESIZE = 0;
|
||||||
|
public static final int LARGEST_ROOT_BTREE_PAGE = 0; // zero when not in auto-vacuum mode
|
||||||
|
|
||||||
|
public static final int ENCODING_UTF8 = 1; // The database text encoding. A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be.
|
||||||
|
|
||||||
|
public static final int USER_VERSION = 0;
|
||||||
|
public static final int VACUUM_MODE_OFF = 0; // not used
|
||||||
|
public static final int APP_ID = 0;
|
||||||
|
public static final byte[] FILLER = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
}; // 20 bytes for some future use
|
||||||
|
public static final byte[] VERSION_VALID_FOR = {0, 0, 0x03, -123};
|
||||||
|
public static final byte[] SQLITE_VERSION = {0x00, 0x2e, 0x5F, 0x1A};
|
||||||
|
|
||||||
|
public static final short NO_FREE_BLOCKS = 0;
|
||||||
|
|
||||||
|
|
||||||
|
public static final byte TABLE_LEAF_PAGE = 0x0d; //TODO enum?
|
||||||
|
public static final byte TABLE_INTERIOR_PAGE = 0x05;
|
||||||
|
public static final byte INDEX_LEAF_PAGE = 0x0a;
|
||||||
|
public static final byte INDEX_INTERIOR_PAGE = 0x02;
|
||||||
|
}
|
||||||
59
src/main/java/sqlighter/SchemaRecord.java
Normal file
59
src/main/java/sqlighter/SchemaRecord.java
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package sqlighter;
|
||||||
|
|
||||||
|
import sqlighter.data.Record;
|
||||||
|
import sqlighter.data.Value;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Is a record in the sqlites_schema table
|
||||||
|
* and a special case of a Record
|
||||||
|
* class is being used for both reading and writing
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class SchemaRecord {
|
||||||
|
|
||||||
|
private final long rowid;
|
||||||
|
private final String tableName;
|
||||||
|
private long rootpage;
|
||||||
|
private final String sql;
|
||||||
|
|
||||||
|
public SchemaRecord(long rowid, String tableName, long rootpage, String sql) {
|
||||||
|
this.rowid = rowid;
|
||||||
|
this.tableName = tableName;
|
||||||
|
this.rootpage = rootpage;
|
||||||
|
this.sql = sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SchemaRecord(long rowid, String tableName, long rootpage) {
|
||||||
|
this(rowid, tableName, rootpage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTableName() {
|
||||||
|
return tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRowid() {
|
||||||
|
return rowid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRootpage() {
|
||||||
|
return rootpage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSql() {
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRootpage(long rootpage) {
|
||||||
|
this.rootpage = rootpage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Record toRecord(){
|
||||||
|
Record record = new Record(rowid);
|
||||||
|
record.addValue(Value.of("table"));
|
||||||
|
record.addValue(Value.of(getTableName().toLowerCase()));
|
||||||
|
record.addValue(Value.of(getTableName().toLowerCase()));
|
||||||
|
record.addValue(Value.of(getRootpage()));
|
||||||
|
record.addValue(Value.of(getSql()));
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/main/java/sqlighter/Varint.java
Normal file
169
src/main/java/sqlighter/Varint.java
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
package sqlighter;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes integers to byte representation like Sqlite's putVarint64
|
||||||
|
*/
|
||||||
|
public final class Varint {
|
||||||
|
private final static byte[] B0 = new byte[0];
|
||||||
|
private final static byte[] B1 = new byte[1];
|
||||||
|
private final static byte[] B2 = new byte[2];
|
||||||
|
private final static byte[] B3 = new byte[3];
|
||||||
|
private final static byte[] B4 = new byte[4];
|
||||||
|
private final static byte[] B5 = new byte[5];
|
||||||
|
private final static byte[] B6 = new byte[6];
|
||||||
|
private final static byte[] B7 = new byte[7];
|
||||||
|
private final static byte[] B8 = new byte[8];
|
||||||
|
private final static byte[] B9 = new byte[9];
|
||||||
|
|
||||||
|
private final static byte[][] B = {B0, B1, B2, B3, B4, B5, B6, B7, B8, B9};
|
||||||
|
|
||||||
|
private Varint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] write(long v) {
|
||||||
|
if ((v & ((0xff000000L) << 32)) != 0) {
|
||||||
|
byte[] result = new byte[9];
|
||||||
|
result[8] = (byte) v;
|
||||||
|
v >>= 8;
|
||||||
|
for (int i = 7; i >= 0; i--) {
|
||||||
|
result[i] = (byte) ((v & 0x7f) | 0x80);
|
||||||
|
v >>= 7;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
int n;
|
||||||
|
byte[] buf = new byte[8];
|
||||||
|
for (n = 0; v != 0; n++, v >>= 7) {
|
||||||
|
buf[n] = (byte) ((v & 0x7f) | 0x80);
|
||||||
|
}
|
||||||
|
buf[0] &= 0x7f;
|
||||||
|
byte[] result = new byte[n];
|
||||||
|
for (int i = 0, j = n - 1; j >= 0; j--, i++) {
|
||||||
|
result[i] = buf[j];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* read a long value from a variable nr of bytes in varint format
|
||||||
|
* NB the end is encoded in the bytes, and the passed byte array may be bigger, but the
|
||||||
|
* remainder is not read. It's up to the caller to do it right.
|
||||||
|
*/
|
||||||
|
public static long read(byte[] bytes) {
|
||||||
|
return read(ByteBuffer.wrap(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* read a long value from a variable nr of bytes in varint format
|
||||||
|
*
|
||||||
|
* copied from the sqlite source, with some java specifics, most notably the addition of
|
||||||
|
* &0xFF for the right conversion from byte => signed in java, but to be interpreted as unsigned,
|
||||||
|
* to long
|
||||||
|
*
|
||||||
|
* Does not have the issue that the read(byte[] bytes) method has. The nr of bytes read is determined
|
||||||
|
* by the varint64 format.
|
||||||
|
*
|
||||||
|
* TODO write specialized version for u32
|
||||||
|
*/
|
||||||
|
public static long read(ByteBuffer buffer) {
|
||||||
|
int SLOT_2_0 = 0x001fc07f;
|
||||||
|
int SLOT_4_2_0 = 0xf01fc07f;
|
||||||
|
|
||||||
|
long a = buffer.get() & 0xFF;
|
||||||
|
if ((a & 0x80) == 0) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
long b = buffer.get() & 0xFF;
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
a &= 0x7F;
|
||||||
|
a = a << 7;
|
||||||
|
a |= b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
a = a << 14;
|
||||||
|
a |= (buffer.get() & 0xFF);
|
||||||
|
if ((a & 0x80) == 0) {
|
||||||
|
a &= SLOT_2_0;
|
||||||
|
b &= 0x7F;
|
||||||
|
b = b << 7;
|
||||||
|
a |= b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
a &= SLOT_2_0;
|
||||||
|
b = b << 14;
|
||||||
|
b |= (buffer.get() & 0xFF);
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
b &= SLOT_2_0;
|
||||||
|
a = a << 7;
|
||||||
|
a |= b;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
b &= SLOT_2_0;
|
||||||
|
long s = a;
|
||||||
|
a = a << 14;
|
||||||
|
int m = buffer.get() & 0xFF;
|
||||||
|
a |= m;
|
||||||
|
if ((a & 0x80) == 0) {
|
||||||
|
b = b << 7;
|
||||||
|
a |= b;
|
||||||
|
s = s >> 18;
|
||||||
|
return (s << 32) | a;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s << 7;
|
||||||
|
s |= b;
|
||||||
|
b = b << 14;
|
||||||
|
b |= (buffer.get() & 0xFF);
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
a &= SLOT_2_0;
|
||||||
|
a = a << 7;
|
||||||
|
a |= b;
|
||||||
|
s = s >> 18;
|
||||||
|
return (s << 32) | a;
|
||||||
|
}
|
||||||
|
|
||||||
|
a = a << 14;
|
||||||
|
a |= (buffer.get() & 0xFF);
|
||||||
|
if ((a & 0x80) == 0) {
|
||||||
|
a &= SLOT_4_2_0;
|
||||||
|
b &= SLOT_2_0;
|
||||||
|
b = b << 7;
|
||||||
|
a |= b;
|
||||||
|
s = s >> 11;
|
||||||
|
return (s << 32) | a;
|
||||||
|
}
|
||||||
|
|
||||||
|
a &= SLOT_2_0;
|
||||||
|
b = b << 14;
|
||||||
|
b |= (buffer.get() & 0xFF);
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
b &= SLOT_4_2_0;
|
||||||
|
a = a << 7;
|
||||||
|
a |= b;
|
||||||
|
s = s >> 4;
|
||||||
|
return (s << 32) | a;
|
||||||
|
}
|
||||||
|
|
||||||
|
a = a << 15;
|
||||||
|
a |= (buffer.get() & 0xFF);
|
||||||
|
b &= SLOT_2_0;
|
||||||
|
|
||||||
|
b = b << 8;
|
||||||
|
a |= b;
|
||||||
|
s = s << 4;
|
||||||
|
b = m;
|
||||||
|
b &= 0x7F;
|
||||||
|
b = b >> 3;
|
||||||
|
s |= b;
|
||||||
|
return (s << 32) | a;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/main/java/sqlighter/data/Record.java
Normal file
102
src/main/java/sqlighter/data/Record.java
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package sqlighter.data;
|
||||||
|
|
||||||
|
import sqlighter.Varint;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record in sqlite database.
|
||||||
|
* Used for reading and writing.
|
||||||
|
*/
|
||||||
|
public final class Record {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suppresses use of the rowIDSequence, to facilitate unittests
|
||||||
|
*/
|
||||||
|
public static boolean useDefaultRowId = false;
|
||||||
|
|
||||||
|
// start at 1
|
||||||
|
|
||||||
|
private final long rowId;
|
||||||
|
|
||||||
|
private final List<Value> values = new ArrayList<>(10);
|
||||||
|
|
||||||
|
public Record(long rowId) {
|
||||||
|
this.rowId = rowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addValues(Value... values) {
|
||||||
|
this.values.addAll(Arrays.asList(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addValue(Value value) {
|
||||||
|
this.values.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* write the record to an array of bytes
|
||||||
|
*/
|
||||||
|
public byte[] toBytes() {
|
||||||
|
int dataLength = getDataLength();
|
||||||
|
byte[] lengthBytes = Varint.write(dataLength);
|
||||||
|
byte[] rowIdBytes = Varint.write(rowId);
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(lengthBytes.length + rowIdBytes.length + dataLength);
|
||||||
|
buffer.put(lengthBytes);
|
||||||
|
buffer.put(rowIdBytes);
|
||||||
|
|
||||||
|
// 'The initial portion of the payload that does not spill to overflow pages.'
|
||||||
|
int lengthOfEncodedColumnTypes = values.stream().map(Value::getDataType).mapToInt(ar -> ar.length).sum() + 1;
|
||||||
|
buffer.put(Varint.write(lengthOfEncodedColumnTypes));
|
||||||
|
|
||||||
|
//types
|
||||||
|
for (Value value : values) {
|
||||||
|
value.writeType(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
//values
|
||||||
|
for (Value value : values) {
|
||||||
|
value.writeValue(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDataLength() {
|
||||||
|
return values.stream().mapToInt(Value::getLength).sum() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRowId() {
|
||||||
|
return rowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public List<Value> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the value at the specified column index (0 based)
|
||||||
|
*/
|
||||||
|
public Value getValue(int column) {
|
||||||
|
return values.get(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Record record = (Record) o;
|
||||||
|
return rowId == record.rowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(rowId);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/main/java/sqlighter/data/Value.java
Normal file
123
src/main/java/sqlighter/data/Value.java
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
package sqlighter.data;
|
||||||
|
|
||||||
|
import sqlighter.Varint;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NB Value classes derive their equality from their identity. I.e. no equals/hashcode
|
||||||
|
*/
|
||||||
|
public class Value {
|
||||||
|
private static final byte FLOAT_TYPE = 7;
|
||||||
|
|
||||||
|
protected final byte[] type;
|
||||||
|
protected final byte[] value;
|
||||||
|
protected final int length;
|
||||||
|
|
||||||
|
protected Value(byte[] type, byte[] value) {
|
||||||
|
this.type = type;
|
||||||
|
this.value = value;
|
||||||
|
this.length = type.length + value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the length of serialType + the length of the value
|
||||||
|
*/
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeType(ByteBuffer buffer) {
|
||||||
|
buffer.put(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getDataType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeValue(ByteBuffer buffer) {
|
||||||
|
buffer.put(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Value of(String value) {
|
||||||
|
return new Value(Varint.write(value == null ? 0 : value.getBytes(StandardCharsets.UTF_8).length * 2L + 13),
|
||||||
|
value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Value of(long value) {
|
||||||
|
byte[] valueAsBytes = getValueAsBytes(value);
|
||||||
|
return new Value(getIntegerType(value, valueAsBytes.length), valueAsBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Value of(double value) {
|
||||||
|
return new Value(new byte[]{FLOAT_TYPE}, ByteBuffer.wrap(new byte[8]).putDouble(0, value).array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Value of(byte[] value) {
|
||||||
|
return new Value(Varint.write(value.length * 2L + 12), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getIntegerType(long value, int bytesLength) {
|
||||||
|
if (value == 0) {
|
||||||
|
return new byte[]{8};
|
||||||
|
} else if (value == 1) {
|
||||||
|
return new byte[]{9};
|
||||||
|
} else {
|
||||||
|
if (bytesLength < 5) {
|
||||||
|
return Varint.write(bytesLength);
|
||||||
|
} else if (bytesLength < 7) {
|
||||||
|
return Varint.write(5);
|
||||||
|
} else return Varint.write(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* static because it's used in the constructor
|
||||||
|
*/
|
||||||
|
public static byte[] getValueAsBytes(long value) {
|
||||||
|
if (value == 0) {
|
||||||
|
return new byte[0];
|
||||||
|
} else if (value == 1) {
|
||||||
|
return new byte[0];
|
||||||
|
} else {
|
||||||
|
return longToBytes(value, getLengthOfByteEncoding(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLengthOfByteEncoding(long value) {
|
||||||
|
long u;
|
||||||
|
if (value < 0) {
|
||||||
|
u = ~value;
|
||||||
|
} else {
|
||||||
|
u = value;
|
||||||
|
}
|
||||||
|
if (u <= 127) {
|
||||||
|
return 1;
|
||||||
|
} else if (u <= 32767) {
|
||||||
|
return 2;
|
||||||
|
} else if (u <= 8388607) {
|
||||||
|
return 3;
|
||||||
|
} else if (u <= 2147483647) {
|
||||||
|
return 4;
|
||||||
|
} else if (u <= 140737488355327L) {
|
||||||
|
return 6;
|
||||||
|
} else {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] longToBytes(long n, int nbytes) {
|
||||||
|
byte[] b = new byte[nbytes];
|
||||||
|
for (int i = 0; i < nbytes; i++) {
|
||||||
|
b[i] = (byte) ((n >> (nbytes - i - 1) * 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/main/java/sqlighter/page/Page.java
Normal file
156
src/main/java/sqlighter/page/Page.java
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package sqlighter.page;
|
||||||
|
|
||||||
|
import sqlighter.Database;
|
||||||
|
import sqlighter.SQLiteConstants;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a SQLite page
|
||||||
|
*/
|
||||||
|
public final class Page {
|
||||||
|
|
||||||
|
public static int POSITION_RIGHTMOST_POINTER = 8; // first position after page header
|
||||||
|
public static int START = 12; // first position after page header
|
||||||
|
|
||||||
|
public static final int POSITION_CELL_COUNT = 3;
|
||||||
|
public static final int START_OF_CONTENT_AREA = 5;
|
||||||
|
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
private final ByteBuffer byteBuffer;
|
||||||
|
private long key;
|
||||||
|
|
||||||
|
private final List<Page> children = new ArrayList<>();
|
||||||
|
|
||||||
|
private int forwardPosition;
|
||||||
|
private int backwardPosition;
|
||||||
|
|
||||||
|
private final PageType type;
|
||||||
|
|
||||||
|
static Page newLeaf() {
|
||||||
|
Page page = new Page(PageType.TABLE_LEAF, Database.PAGE_SIZE);
|
||||||
|
page.putU8(SQLiteConstants.TABLE_LEAF_PAGE);
|
||||||
|
page.skipForward(2);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Page newInterior() {
|
||||||
|
Page page = new Page(PageType.TABLE_INTERIOR, Database.PAGE_SIZE);
|
||||||
|
page.putU8(SQLiteConstants.TABLE_INTERIOR_PAGE);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Page newHeader(int size) {
|
||||||
|
return new Page(PageType.HEADER, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChild(Page child) {
|
||||||
|
children.add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Page(PageType type, int size) {
|
||||||
|
this.type = type;
|
||||||
|
data = new byte[size];
|
||||||
|
this.byteBuffer = ByteBuffer.wrap(data);
|
||||||
|
forwardPosition = 0;
|
||||||
|
backwardPosition = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getForwardPosition() {
|
||||||
|
return forwardPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBackwardPosition() {
|
||||||
|
return backwardPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setForwardPosition(int forwardPosition) {
|
||||||
|
this.forwardPosition = forwardPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putU16(int value) {
|
||||||
|
data[forwardPosition] = (byte) ((value >> 8) & 0xFF);
|
||||||
|
data[forwardPosition + 1] = (byte) (value & 0xFF);
|
||||||
|
forwardPosition += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putU32(long value) {
|
||||||
|
data[forwardPosition] = (byte) ((value >> 24) & 0xFF);
|
||||||
|
data[forwardPosition + 1] = (byte) ((value >> 16) & 0xFF);
|
||||||
|
data[forwardPosition + 2] = (byte) ((value >> 8) & 0xFF);
|
||||||
|
data[forwardPosition + 3] = (byte) (value & 0xFF);
|
||||||
|
forwardPosition += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putU8(int value) {
|
||||||
|
data[forwardPosition] = (byte) (value & 0xFF);
|
||||||
|
forwardPosition += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putU8(byte[] value) {
|
||||||
|
System.arraycopy(value, 0, data, forwardPosition, value.length);
|
||||||
|
forwardPosition += value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getU16() {
|
||||||
|
return ((data[forwardPosition] & 0xFF) << 8) + (data[forwardPosition + 1] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putBackward(byte[] value) {
|
||||||
|
backwardPosition -= value.length;
|
||||||
|
System.arraycopy(value, 0, data, backwardPosition, value.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(long key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Page> getChildren() {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getDataBuffer(){
|
||||||
|
// byteBuffer.clear();
|
||||||
|
// byteBuffer.put(data); // someone mentioned that this single write to the (direct) bytebuffer
|
||||||
|
// // from a byte array is the fastest way to use it
|
||||||
|
// byteBuffer.flip();
|
||||||
|
return byteBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skipForward(int length) {
|
||||||
|
this.forwardPosition += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLeaf() {
|
||||||
|
return type == PageType.TABLE_LEAF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInterior() {
|
||||||
|
return type == PageType.TABLE_INTERIOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PageType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
this.forwardPosition = 0;
|
||||||
|
this.backwardPosition = Database.PAGE_SIZE;
|
||||||
|
this.children.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/sqlighter/page/PageCache.java
Normal file
38
src/main/java/sqlighter/page/PageCache.java
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package sqlighter.page;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
public class PageCache {
|
||||||
|
|
||||||
|
protected final Queue<Page> leafPages = new LinkedBlockingQueue<>();
|
||||||
|
protected final Queue<Page> interiorPages = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
public Page getInteriorPage() {
|
||||||
|
Page page = interiorPages.poll();
|
||||||
|
if (page == null) {
|
||||||
|
page = Page.newInterior();
|
||||||
|
} else {
|
||||||
|
page.reset();
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page getLeafPage() {
|
||||||
|
Page page = leafPages.poll();
|
||||||
|
if (page == null) {
|
||||||
|
page = Page.newLeaf();
|
||||||
|
} else {
|
||||||
|
page.reset();
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release(Page page) {
|
||||||
|
if (page.getType() == PageType.TABLE_INTERIOR) {
|
||||||
|
interiorPages.add(page);
|
||||||
|
} else if (page.getType() == PageType.TABLE_LEAF) {
|
||||||
|
leafPages.add(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/sqlighter/page/PageCacheFactory.java
Normal file
20
src/main/java/sqlighter/page/PageCacheFactory.java
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package sqlighter.page;
|
||||||
|
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class PageCacheFactory {
|
||||||
|
|
||||||
|
private static final ThreadLocal<SoftReference<PageCache>> threadlocalPageCache = new ThreadLocal<>();
|
||||||
|
|
||||||
|
|
||||||
|
public static PageCache getPageCache() {
|
||||||
|
return Optional.ofNullable(threadlocalPageCache.get())
|
||||||
|
.map(SoftReference::get)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
PageCache pageCache = new PageCache();
|
||||||
|
threadlocalPageCache.set(new SoftReference<>(pageCache));
|
||||||
|
return pageCache;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/sqlighter/page/PageType.java
Normal file
9
src/main/java/sqlighter/page/PageType.java
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package sqlighter.page;
|
||||||
|
|
||||||
|
public enum PageType {
|
||||||
|
TABLE_LEAF,
|
||||||
|
TABLE_INTERIOR,
|
||||||
|
INDEX_LEAF,
|
||||||
|
INDEX_INTERIOR,
|
||||||
|
HEADER,
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 187.496 187.496" style="enable-background:new 0 0 187.496 187.496;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<path d="M93.748,0C42.056,0,0,42.055,0,93.748s42.056,93.748,93.748,93.748s93.748-42.055,93.748-93.748S145.44,0,93.748,0z
|
|
||||||
M93.748,173.496C49.774,173.496,14,137.721,14,93.748S49.774,14,93.748,14s79.748,35.775,79.748,79.748
|
|
||||||
S137.722,173.496,93.748,173.496z"/>
|
|
||||||
<path d="M102.028,54.809h-26.53c-3.866,0-7,3.134-7,7v31.939v31.939c0,3.866,3.134,7,7,7s7-3.134,7-7v-24.939h19.53
|
|
||||||
c12.666,0,22.97-10.304,22.97-22.97C124.998,65.113,114.694,54.809,102.028,54.809z M102.028,86.748h-19.53V68.809h19.53
|
|
||||||
c4.946,0,8.97,4.024,8.97,8.97S106.975,86.748,102.028,86.748z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 951 B |
Loading…
Add table
Reference in a new issue