/* Copyright (c) 2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Justin Deoliveira (Boundless) - initial implementation */ package org.locationtech.geogig.storage.sqlite; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; import static org.locationtech.geogig.storage.sqlite.SQLiteStorage.FORMAT_NAME; import static org.locationtech.geogig.storage.sqlite.SQLiteStorage.VERSION; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Platform; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.RevFeature; import org.locationtech.geogig.api.RevFeatureType; import org.locationtech.geogig.api.RevObject; import org.locationtech.geogig.api.RevTag; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.repository.RepositoryConnectionException; import org.locationtech.geogig.storage.BulkOpListener; import org.locationtech.geogig.storage.ConfigDatabase; import org.locationtech.geogig.storage.ObjectDatabase; import org.locationtech.geogig.storage.ObjectInserter; import org.locationtech.geogig.storage.ObjectSerializingFactory; import org.locationtech.geogig.storage.datastream.DataStreamSerializationFactoryV1; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.Lists; /** * Base class for SQLite based object database. * * @author Justin Deoliveira, Boundless * * @param <C> Connection type. */ public abstract class SQLiteObjectDatabase<C> implements ObjectDatabase { final Platform platform; final ConfigDatabase configdb; final ObjectSerializingFactory serializer = DataStreamSerializationFactoryV1.INSTANCE; C cx; public SQLiteObjectDatabase(ConfigDatabase configdb, Platform platform) { this.configdb = configdb; this.platform = platform; } @Override public void open() { if (cx == null) { cx = connect(SQLiteStorage.geogigDir(platform)); init(cx); } } @Override public void configure() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.OBJECT.configure(configdb, FORMAT_NAME, VERSION); } @Override public void checkConfig() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.OBJECT.verify(configdb, FORMAT_NAME, VERSION); } @Override public boolean isOpen() { return cx != null; } @Override public void close() { if (cx != null) { close(cx); cx = null; } } @Override public boolean exists(ObjectId id) { return has(id.toString(), cx); } @Override public List<ObjectId> lookUp(String partialId) { return Lists.newArrayList(transform(search(partialId, cx), StringToObjectId.INSTANCE)); } @Override public RevObject get(ObjectId id) throws IllegalArgumentException { RevObject obj = getIfPresent(id); if (obj == null) { throw new NoSuchElementException("No object with id: " + id); } return obj; } @Override public <T extends RevObject> T get(ObjectId id, Class<T> type) throws IllegalArgumentException { T obj = getIfPresent(id, type); if (obj == null) { throw new NoSuchElementException("No object with ids: " + id); } return obj; } @Override public RevObject getIfPresent(ObjectId id) { InputStream bytes = get(id.toString(), cx); return readObject(bytes, id); } @Override public <T extends RevObject> T getIfPresent(ObjectId id, Class<T> type) throws IllegalArgumentException { RevObject obj = getIfPresent(id); return obj != null ? type.cast(obj) : null; } @Override public RevTree getTree(ObjectId id) { return get(id, RevTree.class); } @Override public RevFeature getFeature(ObjectId id) { return get(id, RevFeature.class); } @Override public RevFeatureType getFeatureType(ObjectId id) { return get(id, RevFeatureType.class); } @Override public RevCommit getCommit(ObjectId id) { return get(id, RevCommit.class); } @Override public RevTag getTag(ObjectId id) { return get(id, RevTag.class); } @Override public Iterator<RevObject> getAll(Iterable<ObjectId> ids) { return getAll(ids, BulkOpListener.NOOP_LISTENER); } @Override public Iterator<RevObject> getAll(Iterable<ObjectId> ids, final BulkOpListener listener) { return filter(transform(ids, new Function<ObjectId, RevObject>() { @Override public RevObject apply(ObjectId id) { RevObject obj = getIfPresent(id); if (obj == null) { listener.notFound(id); } else { listener.found(id, null); } return obj; } }), Predicates.notNull()).iterator(); } @Override public boolean put(RevObject object) { String id = object.getId().toString(); try { put(id, writeObject(object), cx); } catch (IOException e) { throw new RuntimeException("Unable to serialize object: " + object); } return true; } @Override public void putAll(Iterator<? extends RevObject> objects) { putAll(objects, BulkOpListener.NOOP_LISTENER); } @Override public void putAll(Iterator<? extends RevObject> objects, BulkOpListener listener) { while (objects.hasNext()) { RevObject obj = objects.next(); if (put(obj)) { listener.inserted(obj.getId(), null); } } } @Override public boolean delete(ObjectId objectId) { return delete(objectId.toString(), cx); } @Override public long deleteAll(Iterator<ObjectId> ids) { return deleteAll(ids, BulkOpListener.NOOP_LISTENER); } @Override public long deleteAll(Iterator<ObjectId> ids, BulkOpListener listener) { long count = 0; while (ids.hasNext()) { ObjectId id = ids.next(); if (delete(id)) { count++; listener.deleted(id); } } return count; } @Override public ObjectInserter newObjectInserter() { return new ObjectInserter(this); } /** * Reads object from its binary representation as stored in the database. */ protected RevObject readObject(InputStream bytes, ObjectId id) { if (bytes == null) { return null; } return serializer.createObjectReader().read(id, bytes); } /** * Writes object to its binary representation as stored in the database. */ protected InputStream writeObject(RevObject object) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); serializer.createObjectWriter(object.getType()).write(object, bout); return new ByteArrayInputStream(bout.toByteArray()); } /** * Opens a database connection, returning the object representing connection state. */ protected abstract C connect(File geogigDir); /** * Closes a database connection. * * @param cx The connection object. */ protected abstract void close(C cx); /** * Creates the object table with the following schema: * * <pre> * objects(id:varchar PRIMARY KEY, object:blob) * </pre> * * Implementations of this method should be prepared to be called multiple times, so must check * if the table already exists. * * @param cx The connection object. */ protected abstract void init(C cx); /** * Determines if the object with the specified id exists. */ protected abstract boolean has(String id, C cx); /** * Searches for objects with ids that match the speciifed partial string. * * @param partialId The partial id. * * @return Iterable of matches. */ protected abstract Iterable<String> search(String partialId, C cx); /** * Retrieves the object with the specified id. * <p> * Must return <code>null</code> if no such object exists. * </p> */ protected abstract InputStream get(String id, C cx); /** * Inserts or updates the object with the specified id. */ protected abstract void put(String id, InputStream obj, C cx); /** * Deletes the object with the specified id. * * @return Flag indicating if object was actually removed. */ protected abstract boolean delete(String id, C cx); }