package org.apache.clerezza.rdf.jena.tdb.storage;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.clerezza.rdf.core.Graph;
import org.apache.clerezza.rdf.core.MGraph;
import org.apache.clerezza.rdf.core.Triple;
import org.apache.clerezza.rdf.core.TripleCollection;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.core.access.EntityAlreadyExistsException;
import org.apache.clerezza.rdf.core.access.EntityUndeletableException;
import org.apache.clerezza.rdf.core.access.NoSuchEntityException;
import org.apache.clerezza.rdf.core.access.QueryableTcProvider;
import org.apache.clerezza.rdf.core.access.TcProvider;
import org.apache.clerezza.rdf.core.access.WeightedTcProvider;
import org.apache.clerezza.rdf.core.impl.TripleImpl;
import org.apache.clerezza.rdf.jena.tdb.internals.ModelGraph;
import org.apache.clerezza.rdf.jena.tdb.internals.Symbols;
import org.apache.clerezza.rdf.jena.tdb.internals.UriRefSet;
import org.apache.clerezza.rdf.ontologies.RDF;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.MapMaker;
import com.hp.hpl.jena.query.Dataset;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.tdb.TDB;
import com.hp.hpl.jena.tdb.TDBFactory;
import org.apache.clerezza.rdf.core.access.TcManager;

/**
 * {@link WeightedTcProvider} implementation for Jena TDB that uses a single
 * {@link TDBFactory#createDataset(String) Dataset} to store all created
 * {@link Graph} and {@link MGraph} instances.<p>
 * The {@link #TDB_DIR} is uses to configure the directory on the disc. It
 * supports property substitution <code>'${property}'</code> based on properties defined
 * in the {@link BundleContext#getProperty(String)} and 
 * {@link System#getProperty(String)}. This is to easily allow configurations
 * such as <code>"${myHome}/myRdfStore"</code><p>
 * The {@link #DEFAULT_GRAPH_NAME} property can be used to define the
 * name of the Graph that exposes the {@link Dataset#getDefaultModel()} as
 * both {@link TcProvider#getGraph(UriRef)} and {@link TcProvider#getMGraph(UriRef)}.
 * This easily allows to access the union graph of the Jena TDB dataset.<p>
 * This {@link TcProvider} {@link ConfigurationPolicy#REQUIRE requires} an
 * configuration and uses the {@link Component#configurationFactory() 
 * configuration factory}. Therefore it will be bot active until a valid
 * configuration is parsed to the {@link ConfigurationAdmin} service. However
 * it supports multiple instances to be created.<p>
 * Users that want to use multiple instances will need to use special filters
 * to ensure that the correct instance is injected to components. As by default
 * the instance with the highest {@link #WEIGHT} will be used by Clerezza
 * to create instances. A good practice to filter for multiple instances is
 * to add an additional user defined key to the configuration that can later
 * be used for filtering. Such additional keys will be savely ignored by
 * this implementation.<p>
 * 
 * @author Rupert Westenthaler, MInto van der Sluis
 *
 */
@Component(metatype=true, immediate=true,
    configurationFactory=true, policy=ConfigurationPolicy.OPTIONAL)
@Service({WeightedTcProvider.class, TcProvider.class, QueryableTcProvider.class})
@Properties(value={
    @Property(name=SingleTdbDatasetTcProvider.TDB_DIR),
    @Property(name=SingleTdbDatasetTcProvider.DEFAULT_GRAPH_NAME),
    @Property(name=SingleTdbDatasetTcProvider.SYNC_INTERVAL, intValue=SingleTdbDatasetTcProvider.DEFAULT_SYNC_INTERVAL),
    @Property(name=SingleTdbDatasetTcProvider.WEIGHT, intValue=105),
    @Property(name=TcManager.GENERAL_PURPOSE_TC, boolValue = true)
})
public class SingleTdbDatasetTcProvider extends BaseTdbTcProvider implements WeightedTcProvider {

    public static final String TDB_DIR = "tdb-dir";
    public static final String DEFAULT_GRAPH_NAME = "default-graph-name";
    public static final String WEIGHT = "weight";
    public static final String SYNC_INTERVAL = "sync-interval";
    public static final String USE_GRAPH_NAME_SUFFIXES = "use-graph-name-suffixes";
    
    public static final int DEFAULT_SYNC_INTERVAL = 6;
    public static final int MIN_SYNC_INTERVAL = 3;
    
    private final Logger log = LoggerFactory.getLogger(SingleTdbDatasetTcProvider.class);

    private int weight;
    private ModelGraph graphNameIndex;
    private int syncInterval = DEFAULT_SYNC_INTERVAL;
    private SyncThread syncThread;

    private final ReadWriteLock datasetLock = new ReentrantReadWriteLock();;
    private UriRef defaultGraphName;

    // Ensure that models not yet garbage collected get properly synced.
    private final ConcurrentMap<UriRef, ModelGraph> syncModels = new MapMaker().weakValues().makeMap();
    
    /**
     * This background thread ensures that changes to {@link Model}s are
     * synchronized with the file system. Only {@link ModelGraph}s where
     * <code>{@link ModelGraph#isReadWrite()} == true</code> are synced.<p>
     * This is similar to the synchronize thread used by the {@link TdbTcProvider}.
     * This thread is started during the 
     * {@link ScalableSingleTdbDatasetTcProvider#activate(ComponentContext) activation}
     * ad the shutdown is requested during 
     * {@link ScalableSingleTdbDatasetTcProvider#deactivate(ComponentContext) deactivation}
     */
    class SyncThread extends Thread {
        private boolean stopRequested = false;

        @Override
        public void run() {
            while (!stopRequested) {
                try {
                    Thread.sleep(syncInterval*1000);
                } catch (InterruptedException ex) {
                    interrupt();
                }
                if (!stopRequested) {
                    datasetLock.writeLock().lock();
                    try {
                        for(ModelGraph mg : syncModels.values()){
                            if(mg.isReadWrite()){
                                mg.sync();
                            } //else we do not need to sync read-only models
                        }
                    } finally {
                        datasetLock.writeLock().unlock();
                    }
                }
            }
        }
        
        public void requestStop() {
            stopRequested = true;
        }
    }

    /**
     * Default constructor used by OSGI
     */
    public SingleTdbDatasetTcProvider(){}
    
    /**
     * Creates a TDB single dataset {@link TcProvider} based on the parsed
     * configuration.<p>
     * The following properties are supported:<ul>
     * <li> {@link #TDB_DIR} (required): The directory used by Jena TDB. Property
     * substitution "${property-name}" with {@link System#getProperties()} is
     * supported.
     * <li> {@link #DEFAULT_GRAPH_NAME}: The name ({@link UriRef}) of the
     * {@link Graph} that exports the union graph. This graph allows to query
     * triples in any named model managed by this {@link TcProvider}.
     * <li> {@link #SYNC_INTERVAL}: The sync interval that
     * is used to write changes in the graph to the file system. If missing
     * the {@link #DEFAULT_SYNC_INTERVAL} is used. Values lower than 
     * {@link #MIN_SYNC_INTERVAL} are ignored
     * <li>{@link #WEIGHT}: The weight of this {@link TcProvider}. If missing
     * <code>0</code> is used as weight.
     * </ul>
     * <b>NOTE</b> Users need to call {@link #close()} to free up system 
     * resources when they are no longer need this instance.
     * @param config The configuration
     * @throws IOException the 
     * @throws ConfigurationException 
     */
    public SingleTdbDatasetTcProvider(Dictionary<String,Object> config) throws ConfigurationException, IOException{
        activate(null,config);
    }
    /**
     * Activate method used by OSGI
     * @param ctx
     * @throws ConfigurationException
     * @throws IOException
     */
    @Activate
    @SuppressWarnings("unchecked")
    protected void activate(ComponentContext ctx) throws ConfigurationException, IOException {
        activate(ctx.getBundleContext(),ctx.getProperties());
    }

    /**
     * Internally used for activation to support  the instantiation via
     * {@link #ScalableSingleTdbDatasetTcProvider(Dictionary)} - to be used outside
     * an OSGI container.
     * @param bc the BundleContext or <code>null</code> if activating outside
     * an OSGI container. The BundleContext is just used to lookup properties
     * for {@link #substituteProperty(String, BundleContext)}.
     * @param config The configuration for this Instance. Note that {@link #TDB_DIR}
     * is required to be present.
     * @throws ConfigurationException if the parsed configuration is invalid
     * @throws IOException on any error while creating/accessing the Jena TDB
     * directory.
     */
    private void activate(BundleContext bc,Dictionary<String,Object> config) throws ConfigurationException, IOException {
        log.info("Activating scalable single Dataset TDB provider");
        Object value = config.get(WEIGHT);
        if(value instanceof Number){
            weight = ((Number)value).intValue();
        } else if(value != null){
            try {
                weight = new BigDecimal(value.toString()).intValueExact();
            } catch (RuntimeException e) {
                throw new ConfigurationException(WEIGHT, "Unable to parse integer weight!", e);
            }
        } else { //weight not defined
            weight = 0;
        }
        value = config.get(SYNC_INTERVAL);
        if(value instanceof Number){
            syncInterval = Math.max(((Number)value).intValue(),MIN_SYNC_INTERVAL);
        } else if(value != null){
            try {
                syncInterval = Math.max(new BigDecimal(value.toString()).intValueExact(),MIN_SYNC_INTERVAL);
            } catch (RuntimeException e) {
                throw new ConfigurationException(SYNC_INTERVAL, "Unable to parse integer weight!", e);
            }
        } else { //sync interval not defined
            syncInterval = DEFAULT_SYNC_INTERVAL;
        }
        value = config.get(TDB_DIR);
        File dataDir;
        if(value != null && !value.toString().isEmpty()){
            dataDir = new File(substituteProperty(value.toString(),bc)).getAbsoluteFile();
        } else {
            value = config.get(Constants.SERVICE_PID);
            if(value == null){
                throw new ConfigurationException(TDB_DIR, "No Data Directory for "
                    + "the Jena TDB store parsed. Also unable to use the "
                    + "'service.pid' property as default because this property "
                    + "is not present in the parsed configuration.");
            }
            dataDir = bc.getDataFile("singleTdb"+File.separatorChar+value.toString());
            log.info("No TDB directory parsed - use default '{}'",dataDir);
        }
        //parse the default graph name
        value = config.get(DEFAULT_GRAPH_NAME);
        if(value != null && !value.toString().isEmpty()){
            try {
                new URI(value.toString());
                defaultGraphName = new UriRef(value.toString());
            } catch (URISyntaxException e) {
                throw new ConfigurationException(DEFAULT_GRAPH_NAME, "The parsed name '"
                        + value + "'for the default graph (union over all "
                		+ "named graphs managed by this Jena TDB dataset) MUST BE "
                        + "an valid URI or NULL do deactivate this feature!",e);
            }
        } else {
            defaultGraphName = null; //deactivate the default graph name
        }
        
        //validate the parsed directory!
        if(!dataDir.exists()){
            if(dataDir.mkdirs()){
                log.info("Created Jena TDB data directory {}",dataDir);
            } else {
                throw new ConfigurationException(TDB_DIR, "Unable to create Jena TDB data directory '"+dataDir+"'!");
            }
        } else if(!dataDir.isDirectory()){
            throw new ConfigurationException("tdb.dir", "Configured jena TDB data directory '"
                    + dataDir+"' already exists, but is not a Directory!");
        } //else exists and is a directory ... nothing to do
        TDB.getContext().set(TDB.symUnionDefaultGraph, true);
        setDataset( TDBFactory.createDataset(dataDir.getAbsolutePath()) );
        graphNameIndex = new ModelGraph(datasetLock, getDataset().getDefaultModel(),true);

        // Remove existing default graph names from the index (if might have changed
        // in the mean time).
        removeDefaultGraphFromIndex();

        //finally ensure the the defaultGraphName is not also used as a graph/mgraph name
        if (defaultGraphName != null) {
          if (isExistingGraphName(defaultGraphName)) {
            throw new ConfigurationException(DEFAULT_GRAPH_NAME, "The configured default graph name '"
                +defaultGraphName+"' is already used as a Graph or MGraph name!");
          } else {
            addToIndex( defaultGraphName, Symbols.Default );
            addToIndex( defaultGraphName, Symbols.Graph );
          }
        }

        syncThread = new SyncThread();
        syncThread.setDaemon(true);
        syncThread.setName("SyncDaemon for Jena TDB "+dataDir.getAbsolutePath());
        syncThread.start();
    }
    
    /**
     * call close in finalisation
     */
    @Override
    protected void finalize() throws Throwable {
        close();
        super.finalize();
    }

    /**
     * Closes this {@link TcProvider} instance and frees up all system resources.
     * This method needs only to be called when using this TcProvider outside
     * an OSGI environment.
     */
    public void close(){
        deactivate(null);
    }

    /**
     * Deactivates this component. Called by the OSGI environment if this
     * component gets deactivated.
     * @param ctx the ComponentContext. May be <code>null</code>
     */
    @Deactivate
    protected void deactivate(ComponentContext ctx) {
        if(syncThread != null){
            syncThread.requestStop();
            syncThread = null;
        }
    	Dataset dataset = getDataset();
        if(dataset != null){ //avoid NPE on multiple calls
            datasetLock.writeLock().lock();
            try {
                for(ModelGraph mg : syncModels.values()){
                    mg.close(); //close also syncs!
                }
                syncModels.clear();

                graphNameIndex.close();
                graphNameIndex = null;

                TDB.sync(dataset);
                dataset.close();
                setDataset(null);
            } finally {
                datasetLock.writeLock().unlock();
            }
        }
    }
    
    /**
     * Internal method used to retrieve an existing Jena {@link ModelGraph} 
     * instance from {@link #syncModels} or initializes a new Jena TDB {@link Model}
     * and Clerezza {@link Graph}s/{@link MGraph}s.
     * @param name the name of the Graph to initialize/create
     * @param readWrite if <code>true</code> a {@link MGraph} is initialized.
     * Otherwise a {@link Graph} is created.
     * @param create if this method is allowed to create an new {@link Model} or
     * if an already existing model is initialized.
     * @return the initialized {@link Model} and @link Graph} or {@link MGraph}.
     * The returned instance will be also cached in {@link #syncModels}. 
     * @throws NoSuchEntityException If <code>create == false</code> and no
     * {@link Model} for the parsed <code>name</code> exists.
     */
    private ModelGraph getModelGraph(UriRef name, boolean readWrite,boolean create) throws NoSuchEntityException {
        ModelGraph modelGraph = null;
        datasetLock.readLock().lock();
        try {
            if(readWrite) {
                // Reuse existing model if not yet garbage collected.
                modelGraph = syncModels.get(name);
            }
            if((modelGraph != null || isExistingGraphName(name)) && create){
                throw new EntityAlreadyExistsException(name);
            } else if(modelGraph == null){
                String modelName = name.getUnicodeString();
                modelGraph = new ModelGraph(datasetLock, name.equals(defaultGraphName) ? 
                		getDataset().getNamedModel("urn:x-arq:UnionGraph") : 
                			getDataset().getNamedModel(modelName),readWrite);
                if(readWrite) {
                    // Keep track of readwrite model to be able to sync them.
                    this.syncModels.put(name, modelGraph);
                }
            }
        } finally {
            datasetLock.readLock().unlock();
        }
        return modelGraph;
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#getGraph(org.apache.clerezza.rdf.core.UriRef)
     */
    @Override
    public Graph getGraph(UriRef name) throws NoSuchEntityException {
        if(name == null){
            throw new IllegalArgumentException("The parsed Graph UriRef MUST NOT be NULL!");
        }
        datasetLock.readLock().lock();
        try {
            if (isExistingGraphName(name, Symbols.Graph) || name.equals(defaultGraphName)){
                return getModelGraph(name,false,false).getGraph();
            } else {
                throw new NoSuchEntityException(name);
            }
        } finally {
            datasetLock.readLock().unlock();
        }
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#getMGraph(org.apache.clerezza.rdf.core.UriRef)
     */
    @Override
    public MGraph getMGraph(UriRef name) throws NoSuchEntityException {
        if(name == null){
            throw new IllegalArgumentException("The parsed Graph UriRef MUST NOT be NULL!");
        }
        datasetLock.readLock().lock();
        try {
            if(isExistingGraphName(name, Symbols.MGraph)){
                return getModelGraph(name,true,false).getMGraph();
            } else {
                throw new NoSuchEntityException(name);
            }
        } finally {
            datasetLock.readLock().unlock();
        }
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#getTriples(org.apache.clerezza.rdf.core.UriRef)
     */
    @Override
    public TripleCollection getTriples(UriRef name) throws NoSuchEntityException {
        if(name == null){
            throw new IllegalArgumentException("The parsed Graph UriRef MUST NOT be NULL!");
        }
        datasetLock.readLock().lock();
        try {
            if(isExistingGraphName(name, Symbols.Graph) || name.equals(defaultGraphName)){
                return getGraph(name);
            } else if(isExistingGraphName(name, Symbols.MGraph)){
                return getMGraph(name);
            } else {
                throw new NoSuchEntityException(name);
            }
        } finally {
            datasetLock.readLock().unlock();
        }
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#listGraphs()
     */
    @Override
    public Set<UriRef> listGraphs() {
        datasetLock.readLock().lock();
        try {
            return new HashSet(new UriRefSet( graphNameIndex, Symbols.Graph ));
        } finally {
            datasetLock.readLock().unlock();
        }
    }

    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#listMGraphs()
     */
    @Override
    public Set<UriRef> listMGraphs() {
        datasetLock.readLock().lock();
        try {
            return  new HashSet(new UriRefSet( graphNameIndex, Symbols.MGraph ));
        } finally {
            datasetLock.readLock().unlock();
        }
    }

    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#listTripleCollections()
     */
    @Override
    public Set<UriRef> listTripleCollections() {
        datasetLock.readLock().lock();
        try {
            return  new HashSet(new UriRefSet( graphNameIndex, null ));
        } finally {
            datasetLock.readLock().unlock();
        }
    }

    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#createMGraph(org.apache.clerezza.rdf.core.UriRef)
     */
    @Override
    public MGraph createMGraph(UriRef name) throws UnsupportedOperationException,
                                           EntityAlreadyExistsException {
        if(name == null){
            throw new IllegalArgumentException("The parsed MGrpah name MUST NOT be NULL!");
        }
        datasetLock.writeLock().lock();
        try {
            if(isExistingGraphName(name)){
                throw new EntityAlreadyExistsException(name);
            }
            MGraph graph = getModelGraph(name,true,true).getMGraph();
            addToIndex( name, Symbols.MGraph);
            return graph;
        } finally {
            datasetLock.writeLock().unlock();
        }
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#createGraph(org.apache.clerezza.rdf.core.UriRef, org.apache.clerezza.rdf.core.TripleCollection)
     */
    @Override
    public Graph createGraph(UriRef name, TripleCollection triples) throws UnsupportedOperationException,
                                                                   EntityAlreadyExistsException {
        if(name == null){
            throw new IllegalArgumentException("The parsed Grpah name MUST NOT be NULL!");
        }
        ModelGraph mg;
        datasetLock.writeLock().lock();
        try {
            if(isExistingGraphName(name)){
                throw new EntityAlreadyExistsException(name);
            }
            mg = getModelGraph(name,false,true);
            addToIndex( name, Symbols.Graph);
            
            //add the parsed data!
            if(triples != null) { //load the initial and final set of triples
                mg.getJenaAdapter().addAll(triples);
                mg.sync();
            }
        } finally {
            datasetLock.writeLock().unlock();
        }
        return mg.getGraph();
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#deleteTripleCollection(org.apache.clerezza.rdf.core.UriRef)
     */
    @Override
    public void deleteTripleCollection(UriRef name) throws UnsupportedOperationException,
                                                   NoSuchEntityException,
                                                   EntityUndeletableException {
        if(name == null){
            throw new IllegalArgumentException("The parsed MGrpah name MUST NOT be NULL!");
        }
        datasetLock.writeLock().lock();
        try {
            if(isExistingGraphName(name,Symbols.MGraph)){
                ModelGraph mg = getModelGraph(name, true, false);
                mg.delete();
                removeFromIndex( name, Symbols.MGraph );
            } else if(isExistingGraphName(name,Symbols.Graph)){
                ModelGraph mg = getModelGraph(name, false, false);
                mg.delete();
                removeFromIndex( name, Symbols.Graph );
            } else if (name.equals(defaultGraphName)){
                throw new EntityUndeletableException(defaultGraphName);
            }
            //delete the graph from the initModels list
            syncModels.remove(name);
        } finally {
            datasetLock.writeLock().unlock();
        }
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.TcProvider#getNames(org.apache.clerezza.rdf.core.Graph)
     */
    @Override
    public Set<UriRef> getNames(Graph graph) {
        //TODO: this method would require to compare the triples within the graph
        //      because an equals check will not work with BNodes. 
        Set<UriRef> graphNames = new HashSet<UriRef>();
        for( Iterator<Triple> iterator = graphNameIndex.getMGraph().iterator(); iterator.hasNext(); ) {
            Triple triple = iterator.next();
            UriRef graphName = new UriRef(triple.getSubject().toString());
            Graph currentGraph = getModelGraph(graphName, false, false).getGraph();
            if(graph.equals(currentGraph)){
                graphNames.add(graphName);
            }
        }
        return graphNames;
    }
    
    /*
     * (non-Javadoc)
     * @see org.apache.clerezza.rdf.core.access.WeightedTcProvider#getWeight()
     */
    @Override
    public int getWeight() {
        return weight;
    }
    
    /**
     * Substitutes ${property.name} with the values retrieved via <ul>
     * <li> {@link BundleContext#getProperty(String)} or
     * <li> {@link System#getProperty(String, String)} if the parsed
     * {@link BundleContext} is <code>null</code>
     * </ul>
     * Substitutes with an empty string if the property is not present. If
     * the substitution does not end with {@link File#separatorChar}, than it is
     * appended to allow easily creating paths relative to root directory available
     * as property regardless if the property includes/excludes the final
     * separator char.
     * <p>
     * Nested substitutions are NOT supported. However multiple substitutions are supported.
     * <p>
     * If someone knows a default implementation feel free to replace!
     * 
     * @param value
     *            the value to substitute
     * @param bundleContext
     *            If not <code>null</code> the {@link BundleContext#getProperty(String)} is used instead of
     *            the {@link System#getProperty(String)}. By that it is possible to use OSGI only properties
     *            for substitution.
     * @return the substituted value
     */
    private static String substituteProperty(String value, BundleContext bundleContext) {
        int prevAt = 0;
        int foundAt = 0;
        StringBuilder substitution = new StringBuilder();
        while ((foundAt = value.indexOf("${", prevAt)) >= prevAt) {
            substitution.append(value.substring(prevAt, foundAt));
            String propertyName = value.substring(foundAt + 2, value.indexOf('}', foundAt));
            String propertyValue = bundleContext == null ? // if no bundleContext is available
            System.getProperty(propertyName) : // use the System properties
                    bundleContext.getProperty(propertyName);
            if(propertyValue != null) {
                substitution.append(propertyValue);
                if(propertyValue.charAt(propertyValue.length()-1) != File.separatorChar){
                    substitution.append(File.separatorChar);
                }
            } //else nothing to append
            prevAt = foundAt + propertyName.length() + 3; // +3 -> "${}".length
        }
        substitution.append(value.substring(prevAt, value.length()));
        return substitution.toString();
    }

    /**
     * Checks whether the given graph name already exists as the specified resource (either graph or mgraph).
     * @param graphName the graph name
     * @param graphType the resource type
     * @return true if a resource with the given name and type already exists, false otherwise.
     */
    private boolean isExistingGraphName(UriRef graphName, UriRef graphType) {
        return graphNameIndex.getMGraph().filter(graphName, RDF.type, graphType).hasNext();
    }

    /**
     * Checks whether the given graph name already exists as either a graph or mgraph.
     * @param graphName the graph name
     * @return true if a graph or mgraph with the given name already exists, false otherwise.
     */
    private boolean isExistingGraphName(UriRef graphName) {
        return isExistingGraphName(graphName, null);
    }
    
    /**
     * Adds a new graphname to the index of graphnames  
     * @param graphName name of the graph
     * @param graphType resourcetype for the graph to add.
     */
    private void addToIndex(UriRef graphName, UriRef graphType) {
        graphNameIndex.getMGraph().add(new TripleImpl(graphName, RDF.type, graphType));
        graphNameIndex.sync();
    }
    
    /**
     * Removes a graphanem from the index of graphnames
     * @param graphName name of the graph to remove
     * @param graphType resource type of the graph to remove.
     */
    private void removeFromIndex(UriRef graphName, UriRef graphType) {
        MGraph index = graphNameIndex.getMGraph();
        Iterator<Triple> triplesToRemove = index.filter(graphName, RDF.type, graphType);
        for( ; triplesToRemove.hasNext(); ) {
            triplesToRemove.next();
            triplesToRemove.remove();
        }
        graphNameIndex.sync();
    }
    
    private void removeDefaultGraphFromIndex() {
      MGraph index = graphNameIndex.getMGraph();
      Iterator<Triple> triplesToRemove = index.filter(null, RDF.type, Symbols.Default);
      for( ; triplesToRemove.hasNext(); ) {
          Triple triple = triplesToRemove.next();
          triplesToRemove.remove();
          removeFromIndex( UriRef.class.cast(triple.getSubject()), Symbols.Graph );
      }
      graphNameIndex.sync();
    }
} 
