/* Copyright 2008 Rickard Öberg. * * 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 org.qi4j.entitystore.jdbm; import jdbm.RecordManager; import jdbm.RecordManagerFactory; import jdbm.RecordManagerOptions; import jdbm.btree.BTree; import jdbm.helper.*; import jdbm.recman.CacheRecordManager; import org.qi4j.api.common.Optional; import org.qi4j.api.configuration.Configuration; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.injection.scope.This; import org.qi4j.api.injection.scope.Uses; import org.qi4j.api.io.Input; import org.qi4j.api.io.Output; import org.qi4j.api.io.Receiver; import org.qi4j.api.io.Sender; import org.qi4j.api.service.Activatable; import org.qi4j.entitystore.map.MapEntityStore; import org.qi4j.library.fileconfig.FileConfiguration; import org.qi4j.library.locking.ReadLock; import org.qi4j.library.locking.WriteLock; import org.qi4j.spi.entity.EntityType; import org.qi4j.spi.entitystore.BackupRestore; import org.qi4j.spi.entitystore.EntityNotFoundException; import org.qi4j.spi.entitystore.EntityStoreException; import org.qi4j.spi.service.ServiceDescriptor; import java.io.*; import java.util.Properties; import java.util.concurrent.locks.ReadWriteLock; /** * JDBM implementation of SerializationStore */ public class JdbmEntityStoreMixin implements Activatable, MapEntityStore, BackupRestore { @Optional @Service FileConfiguration fileConfiguration; @This private Configuration<JdbmConfiguration> config; @Uses private ServiceDescriptor descriptor; private RecordManager recordManager; private BTree index; private Serializer serializer; @This ReadWriteLock lock; // Activatable implementation @SuppressWarnings({"ResultOfMethodCallIgnored"}) public void activate() throws Exception { initialize(); } public void passivate() throws Exception { recordManager.close(); } @ReadLock public Reader get(EntityReference entityReference) throws EntityStoreException { try { Long stateIndex = getStateIndex(entityReference.identity()); if (stateIndex == null) { throw new EntityNotFoundException(entityReference); } byte[] serializedState = (byte[]) recordManager.fetch(stateIndex, serializer); if (serializedState == null) { throw new EntityNotFoundException(entityReference); } return new StringReader(new String(serializedState, "UTF-8")); } catch (IOException e) { throw new EntityStoreException(e); } } @WriteLock public void applyChanges(MapChanges changes) throws IOException { try { changes.visitMap(new MapChanger() { public Writer newEntity(final EntityReference ref, EntityType entityType) throws IOException { return new StringWriter(1000) { @Override public void close() throws IOException { super.close(); byte[] stateArray = toString().getBytes("UTF-8"); long stateIndex = recordManager.insert(stateArray, serializer); String indexKey = ref.toString(); index.insert(indexKey.getBytes("UTF-8"), stateIndex, false); } }; } public Writer updateEntity(final EntityReference ref, EntityType entityType) throws IOException { return new StringWriter(1000) { @Override public void close() throws IOException { super.close(); Long stateIndex = getStateIndex(ref.toString()); byte[] stateArray = toString().getBytes("UTF-8"); recordManager.update(stateIndex, stateArray, serializer); } }; } public void removeEntity(EntityReference ref, EntityType entityType) throws EntityNotFoundException { try { Long stateIndex = getStateIndex(ref.toString()); recordManager.delete(stateIndex); index.remove(ref.toString().getBytes("UTF-8")); } catch (IOException e) { throw new EntityStoreException(e); } } }); recordManager.commit(); } catch (Exception e) { recordManager.rollback(); if (e instanceof IOException) { throw (IOException) e; } else if (e instanceof EntityStoreException) { throw (EntityStoreException) e; } else { IOException exception = new IOException(); exception.initCause(e); throw exception; } } } public Input<Reader, IOException> entityStates() { return new Input<Reader, IOException>() { @Override public <ReceiverThrowableType extends Throwable> void transferTo(Output<? super Reader, ReceiverThrowableType> output) throws IOException, ReceiverThrowableType { lock.writeLock().lock(); try { output.receiveFrom(new Sender<Reader, IOException>() { @Override public <ReceiverThrowableType extends Throwable> void sendTo(Receiver<? super Reader, ReceiverThrowableType> receiver) throws ReceiverThrowableType, IOException { final TupleBrowser browser = index.browse(); final Tuple tuple = new Tuple(); while (browser.getNext(tuple)) { String id = new String((byte[]) tuple.getKey(), "UTF-8"); Long stateIndex = getStateIndex(id); if (stateIndex == null) { continue; } // Skip this one byte[] serializedState = (byte[]) recordManager.fetch(stateIndex, serializer); receiver.receive(new StringReader(new String(serializedState, "UTF-8"))); } } }); } finally { lock.writeLock().unlock(); } } }; } public Input<String, IOException> backup() { return new Input<String, IOException>() { @Override public <ReceiverThrowableType extends Throwable> void transferTo(Output<? super String, ReceiverThrowableType> output) throws IOException, ReceiverThrowableType { lock.readLock().lock(); try { output.receiveFrom(new Sender<String, IOException>() { @Override public <ReceiverThrowableType extends Throwable> void sendTo(Receiver<? super String, ReceiverThrowableType> receiver) throws ReceiverThrowableType, IOException { final TupleBrowser browser = index.browse(); final Tuple tuple = new Tuple(); while (browser.getNext(tuple)) { String id = new String((byte[]) tuple.getKey(), "UTF-8"); Long stateIndex = getStateIndex(id); if (stateIndex == null) { continue; } // Skip this one byte[] serializedState = (byte[]) recordManager.fetch(stateIndex, serializer); receiver.receive(new String(serializedState, "UTF-8")); } } }); } finally { lock.readLock().unlock(); } } }; } public Output<String, IOException> restore() { return new Output<String, IOException>() { @Override public <SenderThrowableType extends Throwable> void receiveFrom(Sender<? extends String, SenderThrowableType> sender) throws IOException, SenderThrowableType { // Create temporary store File tempDatabase = File.createTempFile("restorejdbm", ".data"); final RecordManager recordManager = RecordManagerFactory.createRecordManager(tempDatabase.getAbsolutePath(), new Properties()); ByteArrayComparator comparator = new ByteArrayComparator(); final BTree index = BTree.createInstance(recordManager, comparator, serializer, new LongSerializer(), 16); recordManager.setNamedObject("index", index.getRecid()); recordManager.commit(); try { sender.sendTo(new Receiver<String, IOException>() { int counter = 0; public void receive(String item) throws IOException { // Commit one batch if ((counter++ % 1000) == 0) { recordManager.commit(); } String id = item.substring("{\"identity\":\"".length()); id = id.substring(0, id.indexOf('"')); // Insert byte[] stateArray = item.getBytes("UTF-8"); long stateIndex = recordManager.insert(stateArray, serializer); index.insert(id.getBytes("UTF-8"), stateIndex, false); } }); } catch (IOException e) { recordManager.close(); tempDatabase.delete(); throw e; } catch (Throwable senderThrowableType) { recordManager.close(); tempDatabase.delete(); throw (SenderThrowableType) senderThrowableType; } // Import went ok - continue recordManager.commit(); lock.writeLock().lock(); try { // Replace old database with new JdbmEntityStoreMixin.this.recordManager.close(); boolean deletedOldDatabase = true; File dbFile = new File(getDatabaseName() + ".db"); File lgFile = new File(getDatabaseName() + ".lg"); deletedOldDatabase &= dbFile.delete(); deletedOldDatabase &= lgFile.delete(); if (!deletedOldDatabase) { throw new IOException("Could not remove old database"); } boolean renamedTempDatabase = true; renamedTempDatabase &= new File(tempDatabase.getAbsolutePath() + ".db").renameTo(dbFile); renamedTempDatabase &= new File(tempDatabase.getAbsolutePath() + ".lg").renameTo(lgFile); if (!renamedTempDatabase) { throw new IOException("Could not replace database with temp database"); } // Start up again initialize(); } finally { lock.writeLock().unlock(); } } }; } private String getDatabaseName() { String pathname = config.configuration().file().get(); if (pathname == null) { if (fileConfiguration != null) { File dataDir = fileConfiguration.dataDirectory(); File jdbmDir = new File(dataDir, descriptor.identity()+"/jdbm.data"); pathname = jdbmDir.getAbsolutePath(); } else { pathname = System.getProperty("user.dir") + "/qi4j/jdbm.data"; } } File dataFile = new File(pathname); File directory = dataFile.getAbsoluteFile().getParentFile(); directory.mkdirs(); String name = dataFile.getAbsolutePath(); return name; } private Properties getProperties() { JdbmConfiguration config = this.config.configuration(); Properties properties = new Properties(); properties.put(RecordManagerOptions.AUTO_COMMIT, config.autoCommit().get().toString()); properties.put(RecordManagerOptions.DISABLE_TRANSACTIONS, config.disableTransactions().get().toString()); return properties; } private Long getStateIndex(String identity) throws IOException { return (Long) index.find(identity.getBytes("UTF-8")); } private void initialize() throws IOException { String name = getDatabaseName(); Properties properties = getProperties(); recordManager = RecordManagerFactory.createRecordManager(name, properties); serializer = new ByteArraySerializer(); recordManager = new CacheRecordManager(recordManager, new MRU(1000)); long recid = recordManager.getNamedObject("index"); if (recid != 0) { index = BTree.load(recordManager, recid); } else { ByteArrayComparator comparator = new ByteArrayComparator(); index = BTree.createInstance(recordManager, comparator, serializer, new LongSerializer(), 16); recordManager.setNamedObject("index", index.getRecid()); } recordManager.commit(); } }