/** * Copyright (c) 2002-2011 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.index.impl.lucene; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.search.Query; import org.apache.lucene.search.Searcher; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.PropertyContainer; import org.neo4j.graphdb.Relationship; import org.neo4j.index.impl.lucene.CommitContext.DocumentContext; import org.neo4j.index.impl.lucene.LuceneCommand.CreateIndexCommand; import org.neo4j.index.impl.lucene.LuceneCommand.DeleteCommand; import org.neo4j.index.impl.lucene.LuceneCommand.RemoveCommand; import org.neo4j.kernel.impl.transaction.xaframework.XaCommand; import org.neo4j.kernel.impl.transaction.xaframework.XaLogicalLog; import org.neo4j.kernel.impl.transaction.xaframework.XaTransaction; class LuceneTransaction extends XaTransaction { private final Map<IndexIdentifier, TxDataBoth> txData = new HashMap<IndexIdentifier, TxDataBoth>(); private final LuceneDataSource dataSource; private final Map<IndexIdentifier,CommandList> commandMap = new HashMap<IndexIdentifier,CommandList>(); LuceneTransaction( int identifier, XaLogicalLog xaLog, LuceneDataSource luceneDs ) { super( identifier, xaLog ); this.dataSource = luceneDs; } <T extends PropertyContainer> void add( LuceneIndex<T> index, T entity, String key, Object value ) { value = value instanceof ValueContext ? ((ValueContext) value).getCorrectValue() : value.toString(); TxDataBoth data = getTxData( index, true ); insert( index, entity, key, value, data.added( true ), data.removed( false ) ); queueCommand( index.newAddCommand( entity, key, value ) ); } private Object getEntityId( PropertyContainer entity ) { return entity instanceof Node ? ((Node) entity).getId() : RelationshipId.of( (Relationship) entity ); } <T extends PropertyContainer> TxDataBoth getTxData( LuceneIndex<T> index, boolean createIfNotExists ) { IndexIdentifier identifier = index.getIdentifier(); TxDataBoth data = txData.get( identifier ); if ( data == null && createIfNotExists ) { data = new TxDataBoth( index ); txData.put( identifier, data ); } return data; } <T extends PropertyContainer> void remove( LuceneIndex<T> index, T entity, String key, Object value ) { value = value instanceof ValueContext ? ((ValueContext) value).getCorrectValue() : value.toString(); TxDataBoth data = getTxData( index, true ); insert( index, entity, key, value, data.removed( true ), data.added( false ) ); queueCommand( index.newRemoveCommand( entity, key, value ) ); } <T extends PropertyContainer> void delete( LuceneIndex<T> index ) { txData.put( index.getIdentifier(), new DeletedTxDataBoth( index ) ); queueCommand( new DeleteCommand( index.getIdentifier() ) ); } private CommandList queueCommand( LuceneCommand command ) { IndexIdentifier indexId = command.indexId; CommandList commands = commandMap.get( indexId ); if ( commands == null ) { commands = new CommandList(); commandMap.put( indexId, commands ); } if ( command instanceof DeleteCommand ) { commands.clear(); } commands.add( command ); commands.incCounter( command ); return commands; } private <T extends PropertyContainer> void insert( LuceneIndex<T> index, T entity, String key, Object value, TxDataHolder insertInto, TxDataHolder removeFrom ) { Object id = getEntityId( entity ); if ( removeFrom != null ) { removeFrom.remove( id, key, value ); } insertInto.add( id, key, value ); } <T extends PropertyContainer> Collection<Long> getRemovedIds( LuceneIndex<T> index, Query query ) { TxDataHolder removed = removedTxDataOrNull( index ); if ( removed == null ) { return Collections.emptySet(); } Collection<Long> ids = removed.query( query, null ); return ids != null ? ids : Collections.<Long>emptySet(); } <T extends PropertyContainer> Collection<Long> getRemovedIds( LuceneIndex<T> index, String key, Object value ) { TxDataHolder removed = removedTxDataOrNull( index ); if ( removed == null ) { return Collections.emptySet(); } Collection<Long> ids = removed.get( key, value ); return ids != null ? ids : Collections.<Long>emptySet(); } <T extends PropertyContainer> Collection<Long> getAddedIds( LuceneIndex<T> index, Query query, QueryContext contextOrNull ) { TxDataHolder added = addedTxDataOrNull( index ); if ( added == null ) { return Collections.emptySet(); } Collection<Long> ids = added.query( query, contextOrNull ); return ids != null ? ids : Collections.<Long>emptySet(); } <T extends PropertyContainer> Collection<Long> getAddedIds( LuceneIndex<T> index, String key, Object value ) { TxDataHolder added = addedTxDataOrNull( index ); if ( added == null ) { return Collections.emptySet(); } Collection<Long> ids = added.get( key, value ); return ids != null ? ids : Collections.<Long>emptySet(); } private <T extends PropertyContainer> TxDataHolder addedTxDataOrNull( LuceneIndex<T> index ) { TxDataBoth data = getTxData( index, false ); if ( data == null ) { return null; } return data.added( false ); } private <T extends PropertyContainer> TxDataHolder removedTxDataOrNull( LuceneIndex<T> index ) { TxDataBoth data = getTxData( index, false ); if ( data == null ) { return null; } return data.removed( false ); } @Override protected void doAddCommand( XaCommand command ) { // we override inject command and manage our own in memory command list } @Override protected void injectCommand( XaCommand command ) { queueCommand( ( LuceneCommand ) command ).incCounter( (LuceneCommand ) command ); } @Override protected void doCommit() { dataSource.getWriteLock(); try { for ( Map.Entry<IndexIdentifier, CommandList> entry : this.commandMap.entrySet() ) { if ( entry.getValue().isEmpty() ) { continue; } IndexIdentifier identifier = entry.getKey(); CommandList commandList = entry.getValue(); IndexType type = identifier == LuceneCommand.CreateIndexCommand.FAKE_IDENTIFIER || !commandList.containsWrites() ? null : dataSource.getType( identifier ); CommitContext context = new CommitContext( dataSource, identifier, type, commandList ); for ( LuceneCommand command : commandList.commands ) { command.perform( context ); } applyDocuments( context.writer, type, context.documents ); if ( context.writer != null ) { dataSource.invalidateIndexSearcher( identifier ); } } dataSource.setLastCommittedTxId( getCommitTxId() ); closeTxData(); } catch ( IOException e ) { throw new RuntimeException( e ); } finally { dataSource.releaseWriteLock(); } } private void applyDocuments( IndexWriter writer, IndexType type, Map<Long, DocumentContext> documents ) throws IOException { for ( Map.Entry<Long, DocumentContext> entry : documents.entrySet() ) { DocumentContext context = entry.getValue(); if ( context.exists ) { if ( LuceneDataSource.documentIsEmpty( context.document ) ) { writer.deleteDocuments( type.idTerm( context.entityId ) ); } else { writer.updateDocument( type.idTerm( context.entityId ), context.document ); } } else { writer.addDocument( context.document ); } } } private void closeTxData() { for ( TxDataBoth data : this.txData.values() ) { data.close(); } this.txData.clear(); } @Override protected void doPrepare() { for ( CommandList list : commandMap.values() ) { for ( LuceneCommand command : list.commands ) { addCommand( command ); } } addAbandonedEntitiesToTheTx(); } private void addAbandonedEntitiesToTheTx() { for ( Map.Entry<IndexIdentifier, TxDataBoth> entry : txData.entrySet() ) { Collection<Long> abandonedIds = entry.getValue().index.abandonedIds; if ( !abandonedIds.isEmpty() ) { CommandList commands = commandMap.get( entry.getKey() ); for ( Long id : abandonedIds ) { RemoveCommand command = new RemoveCommand( entry.getKey(), entry.getKey().entityTypeByte, id, null, null ); addCommand( command ); commands.add( command ); } abandonedIds.clear(); } } } @Override protected void doRollback() { commandMap.clear(); closeTxData(); } @Override public boolean isReadOnly() { return commandMap.isEmpty(); } // Bad name private class TxDataBoth { private TxDataHolder add; private TxDataHolder remove; final LuceneIndex index; public TxDataBoth( LuceneIndex index ) { this.index = index; } TxDataHolder added( boolean createIfNotExists ) { if ( this.add == null && createIfNotExists ) { this.add = new TxDataHolder( index, index.type.newTxData( index ) ); } return this.add; } TxDataHolder removed( boolean createIfNotExists ) { if ( this.remove == null && createIfNotExists ) { this.remove = new TxDataHolder( index, index.type.newTxData( index ) ); } return this.remove; } void close() { safeClose( add ); safeClose( remove ); } private void safeClose( TxDataHolder data ) { if ( data != null ) { data.close(); } } } private class DeletedTxDataBoth extends TxDataBoth { public DeletedTxDataBoth( LuceneIndex index ) { super( index ); } @Override TxDataHolder added( boolean createIfNotExists ) { throw illegalStateException(); } @Override TxDataHolder removed( boolean createIfNotExists ) { throw illegalStateException(); } private IllegalStateException illegalStateException() { throw new IllegalStateException( "This index (" + index.getIdentifier() + ") has been marked as deleted in this transaction" ); } } static class CommandList { private final List<LuceneCommand> commands = new ArrayList<LuceneCommand>(); private boolean containsWrites; void add( LuceneCommand command ) { this.commands.add( command ); } boolean containsWrites() { return containsWrites; } void clear() { commands.clear(); containsWrites = false; } void incCounter( LuceneCommand command ) { if ( command.isConsideredNormalWriteCommand() ) { containsWrites = true; } } boolean isEmpty() { return commands.isEmpty(); } boolean isRecovery() { return commands.get( 0 ).isRecovered(); } } void createIndex( Class<? extends PropertyContainer> entityType, String name, Map<String, String> config ) { byte entityTypeByte = 0; if ( entityType == Node.class ) { entityTypeByte = LuceneCommand.NODE; } else if ( entityType == Relationship.class ) { entityTypeByte = LuceneCommand.RELATIONSHIP; } else { throw new IllegalArgumentException( "Unknown entity typee " + entityType ); } queueCommand( new CreateIndexCommand( entityTypeByte, name, config ) ); } <T extends PropertyContainer> Searcher getAdditionsAsSearcher( LuceneIndex<T> index, QueryContext context ) { TxDataHolder data = addedTxDataOrNull( index ); return data != null ? data.asSearcher( context ) : null; } }