/** * 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.kernel.impl.index; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.PropertyContainer; import org.neo4j.graphdb.Relationship; import org.neo4j.kernel.impl.util.IoPrimitiveUtils; public class IndexStore { public static final String INDEX_DB_FILE_NAME = "index.db"; private static final byte[] MAGICK = new byte[] { 'n', 'e', 'o', '4', 'j', '-', 'i', 'n', 'd', 'e', 'x' }; private static final int VERSION = 1; private final File file; private final File oldFile; private final Map<String, Map<String, String>> nodeConfig = new ConcurrentHashMap<String, Map<String,String>>(); private final Map<String, Map<String, String>> relConfig = new ConcurrentHashMap<String, Map<String,String>>(); private ByteBuffer dontUseBuffer = ByteBuffer.allocate( 100 ); public IndexStore( String graphDbStoreDir ) { this.file = new File( new File( graphDbStoreDir ), INDEX_DB_FILE_NAME ); this.oldFile = new File( file.getParentFile(), file.getName() + ".old" ); read(); } private ByteBuffer buffer( int size ) { if ( dontUseBuffer.capacity() < size ) { dontUseBuffer = ByteBuffer.allocate( size*2 ); } return dontUseBuffer; } private void read() { File fileToReadFrom = file.exists() ? file : oldFile; if ( !fileToReadFrom.exists() ) { return; } FileChannel channel = null; try { channel = new RandomAccessFile( fileToReadFrom, "r" ).getChannel(); Integer version = tryToReadVersion( channel ); if ( version == null ) { close( channel ); channel = new RandomAccessFile( fileToReadFrom, "r" ).getChannel(); // Legacy format, TODO readMap( channel, nodeConfig, version ); relConfig.putAll( nodeConfig ); } else if ( version < VERSION ) { // ...add version upgrade code here throw new UnsupportedOperationException( "" + version ); } else { readMap( channel, nodeConfig, readNextInt( channel ) ); readMap( channel, relConfig, readNextInt( channel ) ); } } catch ( IOException e ) { throw new RuntimeException( e ); } finally { close( channel ); } } private Map<String, Map<String, String>> readMap( FileChannel channel, Map<String, Map<String, String>> map, Integer sizeOrTillEof ) throws IOException { for ( int i = 0; sizeOrTillEof == null || i < sizeOrTillEof; i++ ) { String indexName = readNextString( channel ); if ( indexName == null ) { break; } Integer propertyCount = readNextInt( channel ); if ( propertyCount == null ) { break; } Map<String, String> properties = new HashMap<String, String>(); for ( int p = 0; p < propertyCount; p++ ) { String key = readNextString( channel ); if ( key == null ) { break; } String value = readNextString( channel ); if ( value == null ) { break; } properties.put( key, value ); } map.put( indexName, properties ); } return Collections.unmodifiableMap( map ); } private Integer tryToReadVersion( ReadableByteChannel channel ) throws IOException { byte[] array = IoPrimitiveUtils.readBytes( channel, new byte[MAGICK.length] ); if ( !Arrays.equals( MAGICK, array ) ) { return null; } return array != null ? readNextInt( channel ) : null; } private void close( FileChannel channel ) { if ( channel != null ) { try { channel.close(); } catch ( IOException e ) { e.printStackTrace(); } } } private Integer readNextInt( ReadableByteChannel channel ) throws IOException { return IoPrimitiveUtils.readInt( channel, buffer( 4 ) ); } private String readNextString( ReadableByteChannel channel ) throws IOException { return IoPrimitiveUtils.readLengthAndString( channel, buffer( 100 ) ); } public boolean has( Class<? extends PropertyContainer> cls, String indexName ) { return map( cls ).containsKey( indexName ); } public Map<String, String> get( Class<? extends PropertyContainer> cls, String indexName ) { return map( cls ).get( indexName ); } public String[] getNames( Class<? extends PropertyContainer> cls ) { Map<String, Map<String, String>> indexMap = map( cls ); return indexMap.keySet().toArray( new String[indexMap.size()] ); } private Map<String, Map<String, String>> map( Class<? extends PropertyContainer> cls ) { if ( cls.equals( Node.class ) ) { return nodeConfig; } else if ( cls.equals( Relationship.class ) ) { return relConfig; } throw new IllegalArgumentException( cls.toString() ); } // Synchronized since only one thread are allowed to write at any given time public synchronized void remove( Class<? extends PropertyContainer> cls, String indexName ) { if ( map( cls ).remove( indexName ) == null ) { throw new RuntimeException( "Index config for '" + indexName + "' not found" ); } write(); } // Synchronized since only one thread are allowed to write at any given time public synchronized void set( Class<? extends PropertyContainer> cls, String name, Map<String, String> config ) { map( cls ).put( name, Collections.unmodifiableMap( config ) ); write(); } // Synchronized since only one thread are allowed to write at any given time public synchronized boolean setIfNecessary( Class<? extends PropertyContainer> cls, String name, Map<String, String> config ) { Map<String, Map<String, String>> map = map( cls ); if ( map.containsKey( name ) ) { return false; } map.put( name, Collections.unmodifiableMap( config ) ); write(); return true; } private void write() { // Write to a .tmp file File tmpFile = new File( this.file.getParentFile(), this.file.getName() + ".tmp" ); write( tmpFile ); // Make sure the .old file doesn't exist, then rename the current one to .old this.oldFile.delete(); if ( this.file.exists() && !this.file.renameTo( this.oldFile ) ) { throw new RuntimeException( "Couldn't rename " + file + " -> " + oldFile ); } // Rename the .tmp file to the current name if ( !tmpFile.renameTo( this.file ) ) { throw new RuntimeException( "Couldn't rename " + tmpFile + " -> " + file ); } this.oldFile.delete(); } private void write( File file ) { FileChannel channel = null; try { channel = new RandomAccessFile( file, "rw" ).getChannel(); channel.write( ByteBuffer.wrap( MAGICK ) ); IoPrimitiveUtils.writeInt( channel, buffer( 4 ), VERSION ); writeMap( channel, nodeConfig ); writeMap( channel, relConfig ); channel.force( false ); } catch ( IOException e ) { throw new RuntimeException( e ); } finally { close( channel ); } } private void writeMap( FileChannel channel, Map<String, Map<String, String>> map ) throws IOException { IoPrimitiveUtils.writeInt( channel, buffer( 4 ), map.size() ); for ( Map.Entry<String, Map<String, String>> entry : map.entrySet() ) { writeString( channel, entry.getKey() ); writeInt( channel, entry.getValue().size() ); for ( Map.Entry<String, String> propertyEntry : entry.getValue().entrySet() ) { writeString( channel, propertyEntry.getKey() ); writeString( channel, propertyEntry.getValue() ); } } } private void writeInt( FileChannel channel, int value ) throws IOException { IoPrimitiveUtils.writeInt( channel, buffer( 4 ), value ); } private void writeString( FileChannel channel, String value ) throws IOException { IoPrimitiveUtils.writeLengthAndString( channel, buffer( 200 ), value ); } }