/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.stanbol.entityhub.core.impl;

import static java.util.Collections.singletonMap;
import static org.apache.stanbol.entityhub.core.utils.SiteUtils.extractSiteMetadata;
import static org.apache.stanbol.entityhub.core.utils.SiteUtils.initEntityMetadata;
import static org.apache.stanbol.entityhub.servicesapi.site.Site.PROHIBITED_SITE_IDS;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.ACCESS_URI;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.CACHE_ID;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.CACHE_STRATEGY;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.DEFAULT_EXPIRE_DURATION;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.DEFAULT_MAPPING_STATE;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.DEFAULT_SYMBOL_STATE;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.ENTITY_DEREFERENCER_TYPE;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.ENTITY_SEARCHER_TYPE;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.QUERY_URI;
import static org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration.SITE_FIELD_MAPPINGS;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

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.PropertyOption;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.ReferenceStrategy;
import org.apache.felix.scr.annotations.Service;
import org.apache.stanbol.commons.namespaceprefix.NamespacePrefixService;
import org.apache.stanbol.commons.stanboltools.offline.OfflineMode;
import org.apache.stanbol.entityhub.core.mapping.DefaultFieldMapperImpl;
import org.apache.stanbol.entityhub.core.mapping.FieldMappingUtils;
import org.apache.stanbol.entityhub.core.mapping.ValueConverterFactory;
import org.apache.stanbol.entityhub.core.model.EntityImpl;
import org.apache.stanbol.entityhub.core.model.InMemoryValueFactory;
import org.apache.stanbol.entityhub.core.query.DefaultQueryFactory;
import org.apache.stanbol.entityhub.core.query.QueryResultListImpl;
import org.apache.stanbol.entityhub.core.site.ReferencedSiteConfigurationImpl;
import org.apache.stanbol.entityhub.core.utils.OsgiUtils;
import org.apache.stanbol.entityhub.core.utils.SiteUtils;
import org.apache.stanbol.entityhub.servicesapi.defaults.NamespaceEnum;
import org.apache.stanbol.entityhub.servicesapi.mapping.FieldMapper;
import org.apache.stanbol.entityhub.servicesapi.mapping.FieldMapping;
import org.apache.stanbol.entityhub.servicesapi.model.Representation;
import org.apache.stanbol.entityhub.servicesapi.model.Entity;
import org.apache.stanbol.entityhub.servicesapi.model.ValueFactory;
import org.apache.stanbol.entityhub.servicesapi.model.rdf.RdfResourceEnum;
import org.apache.stanbol.entityhub.servicesapi.query.FieldQuery;
import org.apache.stanbol.entityhub.servicesapi.query.FieldQueryFactory;
import org.apache.stanbol.entityhub.servicesapi.query.QueryResultList;
import org.apache.stanbol.entityhub.servicesapi.site.EntityDereferencer;
import org.apache.stanbol.entityhub.servicesapi.site.EntitySearcher;
import org.apache.stanbol.entityhub.servicesapi.site.License;
import org.apache.stanbol.entityhub.servicesapi.site.Site;
import org.apache.stanbol.entityhub.servicesapi.site.ReferencedSiteConfiguration;
import org.apache.stanbol.entityhub.servicesapi.site.SiteException;
import org.apache.stanbol.entityhub.servicesapi.site.SiteConfiguration;
import org.apache.stanbol.entityhub.servicesapi.yard.Cache;
import org.apache.stanbol.entityhub.servicesapi.yard.CacheStrategy;
import org.apache.stanbol.entityhub.servicesapi.yard.Yard;
import org.apache.stanbol.entityhub.servicesapi.yard.YardException;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.ComponentFactory;
import org.osgi.service.component.ComponentInstance;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This in the Default implementation of the {@link Site} interface. However this implementation forwards
 * calls to methods defined within the {@link EntityDereferencer} and {@link EntitySearcher} to sub components
 * (See the detailed description below).
 * <p>
 * Each {@link Site} with an {@link CacheStrategy} other than {@link CacheStrategy#none} needs an associated
 * {@link Cache}.
 * <p>
 * The Initialisation of the sub-components:
 * <ul>
 * <li><b>{@link EntityDereferencer}:</b> Implementations of this interface are specific to the used
 * protocol/technology of the referenced site. Because of that calls to methods defined in this interface are
 * forwarded to an site specific instance of the {@link EntityDereferencer} interface as configured by the
 * {@link SiteConfiguration#ENTITY_DEREFERENCER_TYPE} property.<br>
 * During activation the the {@link BundleContext} is used to search for {@link ComponentFactory} with the
 * configuration <code>
 *      "component.name= {@link ComponentContext#getProperties()}.get(
 *      {@link SiteConfiguration#ENTITY_DEREFERENCER_TYPE})</code>. This factory is used to create an instance
 * of {@link EntityDereferencer}. <br>
 * Note also, that the configuration of this instance that is covered by the {@link SiteConfiguration}
 * interface are parsed to the {@link EntityDereferencer} instance.
 * <li><b> {@link EntitySearcher}:</b> Implementations of this interface are also specific to the used
 * protocol/technology of the referenced site. Because of that calls to methods defined in this interface are
 * forwarded to an site specific instance of the {@link EntitySearcher} interface as configured by the
 * {@link SiteConfiguration#ENTITY_SEARCHER_TYPE} property.<br>
 * The initialisation of this instance works similar as described for the {@link EntityDereferencer}. However
 * if the value of the {@link SiteConfiguration#ENTITY_SEARCHER_TYPE} is equals to
 * {@link SiteConfiguration#ENTITY_DEREFERENCER_TYPE} or the {@link SiteConfiguration#ENTITY_SEARCHER_TYPE} is
 * not defined at all, than the Dereferencer Instance is also used as {@link EntitySearcher}. If the according
 * cast does not succeed, an {@link ConfigurationException} for the
 * {@link SiteConfiguration#ENTITY_SEARCHER_TYPE} property is thrown.
 * <li><b>{@link Cache}: </b> An instance of a {@link Cache} is used to cache {@link Representation}s loaded
 * form the Site. A cache is a wrapper over a {@link Yard} instance that allows to configure what data are
 * stored for each representation cached form this referenced site. A {@link ServiceTracker} is used for
 * managing the dependency with the cache. So if a cache is no longer available a referenced site can still be
 * used - only the local cache can not be used to retrieve entity representations.
 * </ul>
 * 
 * @author Rupert Westenthaler
 * 
 */
@Component(name = "org.apache.stanbol.entityhub.site.referencedSite",
    configurationFactory = true,
    policy = ConfigurationPolicy.REQUIRE, // the baseUri is required!
    specVersion = "1.1",
    metatype = true,
    immediate = true)
@Service(value = Site.class)
@Properties(value = {
        @Property(name = SiteConfiguration.ID),
        @Property(name = SiteConfiguration.NAME),
        @Property(name = SiteConfiguration.DESCRIPTION),
        @Property(name = SiteConfiguration.ENTITY_PREFIX, cardinality = 1000),
        @Property(name = ACCESS_URI),
        @Property(name = ENTITY_DEREFERENCER_TYPE,
            options = {
                    @PropertyOption(value = '%' + ENTITY_DEREFERENCER_TYPE + ".option.none", name = ""),
                    @PropertyOption(value = '%' + ENTITY_DEREFERENCER_TYPE + ".option.sparql",
                        name = "org.apache.stanbol.entityhub.dereferencer.SparqlDereferencer"),
                    @PropertyOption(value = '%' + ReferencedSiteConfiguration.ENTITY_DEREFERENCER_TYPE
                            + ".option.coolUri",
                        name = "org.apache.stanbol.entityhub.dereferencer.CoolUriDereferencer")},
            value = "org.apache.stanbol.entityhub.dereferencer.SparqlDereferencer"),
        @Property(name = QUERY_URI), // the deri server has better performance
        @Property(name = ENTITY_SEARCHER_TYPE,
            options = {
                    @PropertyOption(value = '%' + ENTITY_SEARCHER_TYPE + ".option.none", name = ""),
                    @PropertyOption(value = '%' + ENTITY_SEARCHER_TYPE + ".option.sparql",
                        name = "org.apache.stanbol.entityhub.searcher.SparqlSearcher"),
                    @PropertyOption(value = '%' + ENTITY_SEARCHER_TYPE + ".option.sparql-virtuoso",
                        name = "org.apache.stanbol.entityhub.searcher.VirtuosoSearcher"),
                    @PropertyOption(value = '%' + ENTITY_SEARCHER_TYPE + ".option.sparql-larq",
                        name = "org.apache.stanbol.entityhub.searcher.LarqSearcher")},
            value = "org.apache.stanbol.entityhub.searcher.SparqlSearcher"),
        @Property(name = DEFAULT_SYMBOL_STATE,
            options = {
                    @PropertyOption(value = '%' + DEFAULT_SYMBOL_STATE + ".option.proposed",
                        name = "proposed"),
                    @PropertyOption(value = '%' + DEFAULT_SYMBOL_STATE + ".option.active", name = "active")},
            value = "proposed"),
        @Property(name = DEFAULT_MAPPING_STATE,
            options = {
                    @PropertyOption(value = '%' + DEFAULT_MAPPING_STATE + ".option.proposed",
                        name = "proposed"),
                    @PropertyOption(value = '%' + DEFAULT_MAPPING_STATE + ".option.confirmed",
                        name = "confirmed")}, value = "proposed"),
        @Property(name = DEFAULT_EXPIRE_DURATION,
            options = {
                    @PropertyOption(value = '%' + DEFAULT_EXPIRE_DURATION + ".option.oneMonth", name = ""
                            + (1000L * 60 * 60 * 24 * 30)),
                    @PropertyOption(value = '%' + DEFAULT_EXPIRE_DURATION + ".option.halfYear", name = ""
                            + (1000L * 60 * 60 * 24 * 183)),
                    @PropertyOption(value = '%' + DEFAULT_EXPIRE_DURATION + ".option.oneYear", name = ""
                            + (1000L * 60 * 60 * 24 * 365)),
                    @PropertyOption(value = '%' + DEFAULT_EXPIRE_DURATION + ".option.none", name = "0")},
            value = "0"),
        @Property(name = CACHE_STRATEGY, options = {
                @PropertyOption(value = '%' + CACHE_STRATEGY + ".option.none", name = "none"),
                @PropertyOption(value = '%' + CACHE_STRATEGY + ".option.used", name = "used"),
                @PropertyOption(value = '%' + CACHE_STRATEGY + ".option.all", name = "all")}, value = "none"),
        @Property(name = CACHE_ID), @Property(name = SITE_FIELD_MAPPINGS, cardinality = 1000)})
public class ReferencedSiteImpl implements Site {
    static final int maxInt = Integer.MAX_VALUE;
    private final Logger log;
    private ComponentContext context;
    private FieldMapper fieldMappings;

    private final Object searcherAndDereferencerLock = new Object();
    private Boolean dereferencerEqualsEntitySearcherComponent;
    private ComponentFactoryListener dereferencerComponentFactoryListener;
    private ComponentFactoryListener searcherComponentFactoryListener;

    // private String dereferencerComponentName;
    private ComponentInstance dereferencerComponentInstance;
    private EntityDereferencer dereferencer;

    // private String entitySearcherComponentName;
    private EntitySearcher entitySearcher;
    private ComponentInstance entitySearcherComponentInstance;

    private ServiceTracker cacheTracker;

    private ReferencedSiteConfiguration siteConfiguration;
    /**
     * Stores keys -> values to be added to the metadata of {@link Entity Entities} created by this site.
     */
    private Map<String,Object> siteMetadata;

    /**
     * The {@link OfflineMode} is used by Stanbol to indicate that no external service should be referenced.
     * For the ReferencedSiteImpl this means that the {@link EntityDereferencer} and {@link EntitySearcher}
     * interfaces are no longer used.
     * <p>
     * 
     * @see #enableOfflineMode(OfflineMode)
     * @see #disableOfflineMode(OfflineMode)
     * @see #isOfflineMode()
     * @see #ensureOnline(String, Class)
     */
    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY,
        policy = ReferencePolicy.DYNAMIC,
        bind = "enableOfflineMode",
        unbind = "disableOfflineMode",
        strategy = ReferenceStrategy.EVENT)
    private OfflineMode offlineMode;

    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
    private NamespacePrefixService nsPrefixService;

    public ReferencedSiteImpl() {
        this(LoggerFactory.getLogger(ReferencedSiteImpl.class));
    }

    protected ReferencedSiteImpl(Logger log) {
        this.log = log;
        log.info("create instance of {}", this.getClass().getName());
    }

    public String getId() {
        return siteConfiguration.getId();
    }

    @Override
    public QueryResultList<Entity> findEntities(FieldQuery query) throws SiteException {
        List<Entity> results;
        if (siteConfiguration.getCacheStrategy() == CacheStrategy.all) {
            // TODO: check if query can be executed based on the base
            // configuration of the Cache
            Cache cache = getCache();
            if (cache != null) {
                try {
                    // When using the Cache, directly get the representations!
                    QueryResultList<Representation> representations = cache.findRepresentation((query));
                    results = new ArrayList<Entity>(representations.size());
                    for (Representation result : representations) {
                        Entity entity = new EntityImpl(getId(), result, null);
                        results.add(entity);
                        initEntityMetadata(entity, siteMetadata,
                            singletonMap(RdfResourceEnum.isChached.getUri(), (Object) Boolean.TRUE));
                    }
                    return new QueryResultListImpl<Entity>(query, results, Entity.class);
                } catch (YardException e) {
                    if (siteConfiguration.getEntitySearcherType() == null || isOfflineMode()) {
                        throw new SiteException("Unable to execute query on Cache "
                                + siteConfiguration.getCacheId(), e);
                    } else {
                        log.warn(
                            String.format(
                                "Error while performing query on Cache %s! Try to use remote site %s as fallback!",
                                siteConfiguration.getCacheId(), siteConfiguration.getQueryUri()), e);
                    }
                }
            } else {
                if (siteConfiguration.getEntitySearcherType() == null || isOfflineMode()) {
                    throw new SiteException(String.format(
                        "Unable to execute query on Cache %s because it is currently not active",
                        siteConfiguration.getCacheId()));
                } else {
                    log.warn(String.format(
                        "Cache %s currently not active will query remote Site %s as fallback",
                        siteConfiguration.getCacheId(), siteConfiguration.getQueryUri()));
                }
            }
        }
        QueryResultList<String> entityIds;
        if (entitySearcher == null) {
            throw new SiteException(String.format("EntitySearcher %s not available for remote site %s!",
                siteConfiguration.getEntitySearcherType(), siteConfiguration.getQueryUri()));
        }
        ensureOnline(siteConfiguration.getQueryUri(), entitySearcher.getClass());
        try {
            log.trace("Will use an entity-searcher [type :: {}][query-uri :: {}].", entitySearcher.getClass()
                    .toString(), EntitySearcher.QUERY_URI);
            entityIds = entitySearcher.findEntities(query);
        } catch (IOException e) {
            throw new SiteException(String.format(
                "Unable to execute query on remote site %s with entitySearcher %s!",
                siteConfiguration.getQueryUri(), siteConfiguration.getEntitySearcherType()), e);
        }
        int numResults = entityIds.size();
        List<Entity> entities = new ArrayList<Entity>(numResults);
        int errors = 0;
        SiteException lastError = null;
        for (String id : entityIds) {
            Entity entity;
            try {
                entity = getEntity(id);
                if (entity == null) {
                    log.warn("Unable to create Entity for ID that was selected by an FieldQuery (id=" + id
                            + ")");
                }
                entities.add(entity);
                // use the position in the list as resultSocre
                entity.getRepresentation().set(RdfResourceEnum.resultScore.getUri(),
                    Float.valueOf((float) numResults));
            } catch (SiteException e) {
                lastError = e;
                errors++;
                log.warn(String
                        .format(
                            "Unable to get Representation for Entity %s. -> %d Error%s for %d Entities in QueryResult (Reason:%s)",
                            id, errors, errors > 1 ? "s" : "", entityIds.size(), e.getMessage()));
            }
            // decrease numResults because it is used as resultScore for
            // entities
            numResults--;
        }
        if (lastError != null) {
            if (entities.isEmpty()) {
                throw new SiteException(
                        "Unable to get anly Representations for Entities selected by the parsed Query (Root-Cause is the last Exception trown)",
                        lastError);
            } else {
                log.warn(String.format("Unable to get %d/%d Represetnations for selected Entities.", errors,
                    entityIds.size()));
                log.warn("Stack trace of the last Exception:", lastError);
            }
        }
        return new QueryResultListImpl<Entity>(query, entities, Entity.class);
    }

    @Override
    public QueryResultList<Representation> find(FieldQuery query) throws SiteException {
        if (siteConfiguration.getCacheStrategy() == CacheStrategy.all) {
            // TODO: check if query can be executed based on the base
            // configuration of the Cache
            Cache cache = getCache();
            if (cache != null) {
                try {
                    return cache.find(query);
                } catch (YardException e) {
                    if (siteConfiguration.getEntitySearcherType() == null || isOfflineMode()) {
                        throw new SiteException("Unable to execute query on Cache "
                                + siteConfiguration.getCacheId(), e);
                    } else {
                        log.warn(
                            String.format(
                                "Error while performing query on Cache %s! Try to use remote site %s as fallback!",
                                siteConfiguration.getCacheId(), siteConfiguration.getQueryUri()), e);
                    }
                }
            } else {
                if (siteConfiguration.getEntitySearcherType() == null || isOfflineMode()) {
                    throw new SiteException(String.format(
                        "Unable to execute query because Cache %s is currently not active",
                        siteConfiguration.getCacheId()));
                } else {
                    log.warn(String.format(
                        "Cache %s currently not active will query remote Site %s as fallback",
                        siteConfiguration.getCacheId(), siteConfiguration.getQueryUri()));
                }
            }
        }
        if (entitySearcher == null) {
            throw new SiteException(String.format("EntitySearcher %s not available for remote site %s!",
                siteConfiguration.getEntitySearcherType(), siteConfiguration.getQueryUri()));
        }
        ensureOnline(siteConfiguration.getQueryUri(), entitySearcher.getClass());
        try {
            return entitySearcher.find(query);
        } catch (IOException e) {
            throw new SiteException("Unable execute Query on remote site " + siteConfiguration.getQueryUri(),
                    e);
        }
    }

    @Override
    public QueryResultList<String> findReferences(FieldQuery query) throws SiteException {
        if (siteConfiguration.getCacheStrategy() == CacheStrategy.all) {
            // TODO: check if query can be executed based on the base
            // configuration of the Cache
            Cache cache = getCache();
            if (cache != null) {
                try {
                    return cache.findReferences(query);
                } catch (YardException e) {
                    if (siteConfiguration.getEntitySearcherType() == null || isOfflineMode()) {
                        throw new SiteException("Unable to execute query on Cache "
                                + siteConfiguration.getCacheId(), e);
                    } else {
                        log.warn(
                            String.format(
                                "Error while performing query on Cache %s! Try to use remote site %s as fallback!",
                                siteConfiguration.getCacheId(), siteConfiguration.getQueryUri()), e);
                    }
                }
            } else {
                if (siteConfiguration.getEntitySearcherType() == null || isOfflineMode()) {
                    throw new SiteException(String.format(
                        "Unable to execute query on Cache %s because it is currently not active",
                        siteConfiguration.getCacheId()));
                } else {
                    log.warn(String.format(
                        "Cache %s currently not active will query remote Site %s as fallback",
                        siteConfiguration.getCacheId(), siteConfiguration.getQueryUri()));
                }
            }
        }
        if (entitySearcher == null) {
            throw new SiteException(String.format("EntitySearcher %s not available for remote site %s!",
                siteConfiguration.getEntitySearcherType(), siteConfiguration.getQueryUri()));
        }
        ensureOnline(siteConfiguration.getQueryUri(), entitySearcher.getClass());
        try {
            return entitySearcher.findEntities(query);
        } catch (IOException e) {
            throw new SiteException("Unable execute Query on remote site " + siteConfiguration.getQueryUri(),
                    e);
        }
    }

    @Override
    public InputStream getContent(String id, String contentType) throws SiteException {
        if (siteConfiguration.getEntityDereferencerType() == null) {
            throw new SiteException(
                    String.format(
                        "Unable to get Content for Entity %s because No dereferencer configured for ReferencedSite %s",
                        id, getId()));
        }
        if (dereferencer == null) {
            throw new SiteException(String.format("Dereferencer %s for remote site %s is not available",
                siteConfiguration.getEntityDereferencerType(), siteConfiguration.getAccessUri()));
        }
        ensureOnline(siteConfiguration.getAccessUri(), dereferencer.getClass());
        try {
            return dereferencer.dereference(id, contentType);
        } catch (IOException e) {
            throw new SiteException(
                    String.format(
                        "Unable to load content for Entity %s and mediaType %s from remote site %s by using dereferencer %s",
                        id, contentType, siteConfiguration.getAccessUri(),
                        siteConfiguration.getEntityDereferencerType()), e);
        }
    }

    @Override
    public Entity getEntity(String id) throws SiteException {
        Cache cache = getCache();
        Entity entity = null;
        long start = System.currentTimeMillis();
        if (cache != null) {
            try {
                Representation rep = cache.getRepresentation(id);
                if (rep != null) {
                    entity = new EntityImpl(getId(), rep, null);
                    initEntityMetadata(entity, siteMetadata,
                        singletonMap(RdfResourceEnum.isChached.getUri(), (Object) Boolean.TRUE));
                } else if (siteConfiguration.getCacheStrategy() == CacheStrategy.all) {
                    return null; // do no remote lokkups on CacheStrategy.all!!
                }
            } catch (YardException e) {
                if (siteConfiguration.getEntityDereferencerType() == null || isOfflineMode()) {
                    throw new SiteException(String.format("Unable to get Represetnation %s form Cache %s",
                        id, siteConfiguration.getCacheId()), e);
                } else {
                    log.warn(
                        String.format(
                            "Unable to get Represetnation %s form Cache %s. Will dereference from remote site %s",
                            id, siteConfiguration.getCacheId(), siteConfiguration.getAccessUri()), e);
                }
            }
        } else {
            if (siteConfiguration.getEntityDereferencerType() == null || isOfflineMode()) {
                throw new SiteException(String.format(
                    "Unable to get Represetnation %s because configured Cache %s is currently not available",
                    id, siteConfiguration.getCacheId()));
            } else {
                log.warn(String.format(
                    "Cache %s is currently not available. Will use remote site %s to load Representation %s",
                    siteConfiguration.getCacheId(), siteConfiguration.getEntityDereferencerType(), id));
            }
        }
        if (entity == null) { // no cache or not found in cache
            if (dereferencer == null) {
                throw new SiteException(String.format(
                    "Entity Dereferencer %s for accessing remote site %s is not available",
                    siteConfiguration.getEntityDereferencerType(), siteConfiguration.getAccessUri()));
            }
            ensureOnline(siteConfiguration.getAccessUri(), dereferencer.getClass());
            Representation rep = null;
            try {
                rep = dereferencer.dereference(id);
            } catch (IOException e) {
                throw new SiteException(String.format(
                    "Unable to load Representation for entity %s form remote site %s with dereferencer %s",
                    id, siteConfiguration.getAccessUri(), siteConfiguration.getEntityDereferencerType()), e);
            }
            // representation loaded from remote site and cache is available
            if (rep != null) {
                Boolean cachedVersion = Boolean.FALSE;
                if (cache != null) {// -> cache the representation
                    try {
                        start = System.currentTimeMillis();
                        // return the the cached version
                        rep = cache.store(rep);
                        cachedVersion = Boolean.TRUE;
                        log.debug("  - cached Representation {} in {} ms", id,
                            (System.currentTimeMillis() - start));
                    } catch (YardException e) {
                        log.warn(String.format(
                            "Unable to cache Represetnation %s in Cache %s! Representation not cached!", id,
                            siteConfiguration.getCacheId()), e);
                    }
                }
                entity = new EntityImpl(getId(), rep, null);
                initEntityMetadata(entity, siteMetadata,
                    singletonMap(RdfResourceEnum.isChached.getUri(), (Object) cachedVersion));
            }
        } else {
            log.debug("  - loaded Representation {} from Cache in {} ms", id,
                (System.currentTimeMillis() - start));
        }
        return entity;
    }

    @Override
    public SiteConfiguration getConfiguration() {
        return siteConfiguration;
    }

    @Override
    public String toString() {
        return siteConfiguration != null ? siteConfiguration.getName() : null;
    }

    @Override
    public int hashCode() {
        return siteConfiguration != null ? getId().hashCode() : -1;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Site) {
            SiteConfiguration osc = ((Site) obj).getConfiguration();
            // this will return false if one of the two sites is not activated
            // -> this should be OK
            return siteConfiguration != null && osc != null && getId().equals(osc.getId());
        } else {
            return false;
        }
    }

    @Override
    public FieldMapper getFieldMapper() {
        return fieldMappings;
    }

    /**
     * In case {@link CacheStrategy#all} this Method returns the query factory of the Cache. Otherwise it
     * returns {@link DefaultQueryFactory#getInstance()}.
     */
    @Override
    public FieldQueryFactory getQueryFactory() {
        FieldQueryFactory factory = null;
        if (siteConfiguration.getCacheStrategy() == CacheStrategy.all) {
            Cache cache = getCache();
            if (cache != null) {
                factory = cache.getQueryFactory();
            }
        }
        if (factory == null) {
            factory = DefaultQueryFactory.getInstance();
        }
        return factory;
    }

    public boolean supportsLocalMode() {
        return siteConfiguration.getCacheStrategy() == CacheStrategy.all && getCache() != null;
    }

    public boolean supportsSearch() {
        return supportsLocalMode() || entitySearcher != null;
    }

    /**
     * Internally used to get the Cache for this site. If {@link CacheStrategy#none}, this methods always
     * returns <code>null</code> , otherwise it returns the Cache for the configured Yard or <code>null</code>
     * if no such Cache is available.
     * 
     * @return the cache or <code>null</code> if {@link CacheStrategy#none} or the configured cache instance
     *         is not available.
     */
    protected Cache getCache() {
        if (siteConfiguration.getCacheStrategy() == CacheStrategy.none) {
            return null;
        } else {
            Cache cache = (Cache) cacheTracker.getService();
            if (cache != null && cache.isAvailable()) {
                return cache;
            } else {
                return null;
            }
        }
    }

    /*--------------------------------------------------------------------------
     *  OSGI LIFECYCLE and LISTENER METHODS
     *--------------------------------------------------------------------------
     */

    @SuppressWarnings("unchecked")
    @Activate
    protected void activate(final ComponentContext context) throws ConfigurationException, YardException,
            InvalidSyntaxException {
        log.debug("in {} activate with properties {}", ReferencedSiteImpl.class.getSimpleName(),
            context.getProperties());
        if (context == null || context.getProperties() == null) {
            throw new IllegalStateException(
                    "No Component Context and/or Dictionary properties object parsed to the acticate methode");
        }
        this.context = context;
        // create the SiteConfiguration based on the parsed properties
        // NOTE that the constructor also validation of the parsed configuration
        siteConfiguration = new ReferencedSiteConfigurationImpl(context.getProperties());
        if (PROHIBITED_SITE_IDS.contains(siteConfiguration.getId().toLowerCase())) {
            throw new ConfigurationException(SiteConfiguration.ID, String.format(
                "The ID '%s' of this Referenced Site is one of the following "
                        + "prohibited IDs: {} (case insensitive)", siteConfiguration.getId(),
                PROHIBITED_SITE_IDS));
        }
        log.info(" > initialise Referenced Site {}", siteConfiguration.getName());
        this.siteMetadata = extractSiteMetadata(siteConfiguration, InMemoryValueFactory.getInstance());

        // if the accessUri is the same as the queryUri and both the
        // dereferencer and
        // the entitySearcher uses the same component, than we need only one
        // component
        // for both dependencies.
        this.dereferencerEqualsEntitySearcherComponent =
        // (1) accessURI == queryURI
                siteConfiguration.getAccessUri() != null
                        && siteConfiguration.getAccessUri().equals(siteConfiguration.getQueryUri())
                        &&
                        // (2) entity dereferencer == entity searcher
                        siteConfiguration.getEntityDereferencerType() != null
                        && siteConfiguration.getEntityDereferencerType().equals(
                            siteConfiguration.getEntitySearcherType());

        // init the fieldMapper based on the configuration
        fieldMappings = new DefaultFieldMapperImpl(ValueConverterFactory.getDefaultInstance());
        if (siteConfiguration.getFieldMappings() != null) {
            log.debug(" > Initialise configured field mappings");
            for (String configuredMapping : siteConfiguration.getFieldMappings()) {
                FieldMapping mapping =
                        FieldMappingUtils.parseFieldMapping(configuredMapping, nsPrefixService);
                if (mapping != null) {
                    log.debug("   - add FieldMapping {}", mapping);
                    fieldMappings.addMapping(mapping);
                }
            }
        }
        // now init the referenced Services
        initDereferencerAndEntitySearcher();

        // If a cache is configured init the ServiceTracker used to manage the
        // Reference to the cache!
        if (siteConfiguration.getCacheId() != null) {
            String cacheFilter =
                    String.format("(&(%s=%s)(%s=%s))", Constants.OBJECTCLASS, Cache.class.getName(),
                        Cache.CACHE_YARD, siteConfiguration.getCacheId());
            cacheTracker =
                    new ServiceTracker(context.getBundleContext(), context.getBundleContext().createFilter(
                        cacheFilter), null);
            cacheTracker.open();
        }
    }

    /**
     * Initialise the dereferencer and searcher component as soon as the according {@link ComponentFactory}
     * gets registered.
     * <p>
     * First this Methods tries to find the according {@link ServiceReference}s directly. If they are not
     * available (e.g. because the component factories are not yet started) than it adds a
     * {@link ServiceListener} for the missing {@link ComponentFactory} that calls the
     * {@link #createDereferencerComponent(ComponentFactory)} and
     * {@link #createEntitySearcherComponent(ComponentFactory)} as soon as the factory gets registered.
     * 
     * @throws InvalidSyntaxException
     *             if the #entitySearcherComponentName or the {@link #dereferencerComponentName} somehow cause
     *             an invalid formated string that can not be used to parse a {@link Filter}.
     */
    private void initDereferencerAndEntitySearcher() throws InvalidSyntaxException {
        if (siteConfiguration.getAccessUri() != null && // initialise only if a
                                                        // accessUri
                !siteConfiguration.getAccessUri().isEmpty() && // is configured
                siteConfiguration.getEntitySearcherType() != null) {
            String componentNameFilterString =
                    String.format("(%s=%s)", "component.name", siteConfiguration.getEntitySearcherType());
            String filterString =
                    String.format("(&(%s=%s)%s)", Constants.OBJECTCLASS, ComponentFactory.class.getName(),
                        componentNameFilterString);
            ServiceReference[] refs =
                    context.getBundleContext().getServiceReferences(ComponentFactory.class.getName(),
                        componentNameFilterString);
            if (refs != null && refs.length > 0) {
                createEntitySearcherComponent((ComponentFactory) context.getBundleContext().getService(
                    refs[0]));
            } else { // service factory not yet available -> add servicelistener
                this.searcherComponentFactoryListener =
                        new ComponentFactoryListener(context.getBundleContext());
                // NOTE: here the filter MUST include also the objectClass!
                context.getBundleContext().addServiceListener(this.searcherComponentFactoryListener,
                    filterString);
            }
            // context.getComponentInstance().dispose();
            // throw an exception to avoid an successful activation
        }
        if (siteConfiguration.getQueryUri() != null
                && // initialise only if a query URI
                !siteConfiguration.getQueryUri().isEmpty()
                && // is configured
                siteConfiguration.getEntityDereferencerType() != null
                && !this.dereferencerEqualsEntitySearcherComponent) {
            String componentNameFilterString =
                    String.format("(%s=%s)", "component.name", siteConfiguration.getEntityDereferencerType());
            String filterString =
                    String.format("(&(%s=%s)%s)", Constants.OBJECTCLASS, ComponentFactory.class.getName(),
                        componentNameFilterString);
            ServiceReference[] refs =
                    context.getBundleContext().getServiceReferences(ComponentFactory.class.getName(),
                        componentNameFilterString);
            if (refs != null && refs.length > 0) {
                createDereferencerComponent((ComponentFactory) context.getBundleContext().getService(refs[0]));
            } else { // service factory not yet available -> add servicelistener
                this.dereferencerComponentFactoryListener =
                        new ComponentFactoryListener(context.getBundleContext());
                this.context.getBundleContext().addServiceListener(this.dereferencerComponentFactoryListener,
                    filterString); // NOTE: here the filter MUST
                                   // include also the objectClass!
            }
        }
    }

    /**
     * Creates the entity searcher component used by this {@link Site} (and configured via the
     * {@link SiteConfiguration#ENTITY_SEARCHER_TYPE} property).
     * <p>
     * If the {@link SiteConfiguration#ENTITY_DEREFERENCER_TYPE} is set to the same vale and the
     * {@link #accessUri} also equals the {@link #queryUri}, than the component created for the entity
     * searcher is also used as dereferencer.
     * 
     * @param factory
     *            The component factory used to create the {@link #entitySearcherComponentInstance}
     */
    @SuppressWarnings("unchecked")
    protected void createEntitySearcherComponent(ComponentFactory factory) {
        // both create*** methods sync on the searcherAndDereferencerLock to
        // avoid
        // multiple component instances because of concurrent calls
        synchronized (this.searcherAndDereferencerLock) {
            if (entitySearcherComponentInstance == null) {
                this.entitySearcherComponentInstance =
                        factory.newInstance(OsgiUtils.copyConfig(context.getProperties()));
                this.entitySearcher = (EntitySearcher) entitySearcherComponentInstance.getInstance();
            }
            if (dereferencerEqualsEntitySearcherComponent) {
                this.dereferencer = (EntityDereferencer) entitySearcher;
            }
        }
    }

    /**
     * Creates the entity dereferencer component used by this {@link Site}. The implementation used as the
     * dereferencer is configured by the {@link SiteConfiguration#ENTITY_DEREFERENCER_TYPE} property.
     * 
     * @param factory
     *            the component factory used to create the {@link #dereferencer}
     */
    @SuppressWarnings("unchecked")
    protected void createDereferencerComponent(ComponentFactory factory) {
        // both create*** methods sync on searcherAndDereferencerLock to avoid
        // multiple component instances because of concurrent calls
        synchronized (this.searcherAndDereferencerLock) {
            if (dereferencerComponentInstance == null) {
                dereferencerComponentInstance =
                        factory.newInstance(OsgiUtils.copyConfig(context.getProperties()));
                this.dereferencer = (EntityDereferencer) dereferencerComponentInstance.getInstance();
            }
        }
    }

    /**
     * Simple {@link ServiceListener} implementation that is used to get notified if one of the
     * {@link ComponentFactory component factories} for the configured implementation of the
     * {@link EntityDereferencer} or {@link EntitySearcher} interfaces get registered.
     * 
     * @author Rupert Westenthaler
     * 
     */
    private class ComponentFactoryListener implements ServiceListener {
        private BundleContext bundleContext;

        protected ComponentFactoryListener(BundleContext bundleContext) {
            if (bundleContext == null) {
                throw new IllegalArgumentException("The BundleContext MUST NOT be NULL!");
            }
            this.bundleContext = bundleContext;
        }

        @Override
        public void serviceChanged(ServiceEvent event) {
            Object eventComponentName = event.getServiceReference().getProperty("component.name");
            if (event.getType() == ServiceEvent.REGISTERED) {
                log.info("Process ServiceEvent for ComponentFactory {} and State REGISTERED",
                    eventComponentName);
                ComponentFactory factory =
                        (ComponentFactory) bundleContext.getService(event.getServiceReference());
                if (siteConfiguration.getEntityDereferencerType() != null
                        && siteConfiguration.getEntityDereferencerType().equals(eventComponentName)) {
                    createDereferencerComponent(factory);
                }
                if (siteConfiguration.getEntitySearcherType() != null
                        && siteConfiguration.getEntitySearcherType().equals(eventComponentName)) {
                    createEntitySearcherComponent(factory);
                }
            } else {
                log.info("Ignore ServiceEvent for ComponentFactory {} and state {}", eventComponentName,
                    event.getType() == ServiceEvent.MODIFIED ? "MODIFIED"
                            : event.getType() == ServiceEvent.UNREGISTERING ? "UNREGISTERING"
                                    : "MODIFIED_ENDMATCH");
            }
        }

    }

    @Deactivate
    protected void deactivate(ComponentContext context) {
        log.info("deactivate Referenced Site {}", siteConfiguration.getName());
        this.dereferencer = null;
        if (this.dereferencerComponentInstance != null) {
            this.dereferencerComponentInstance.dispose();
            this.dereferencerComponentInstance = null;
        }
        this.entitySearcher = null;
        if (this.entitySearcherComponentInstance != null) {
            this.entitySearcherComponentInstance.dispose();
            this.entitySearcherComponentInstance = null;
        }
        if (searcherComponentFactoryListener != null) {
            context.getBundleContext().removeServiceListener(searcherComponentFactoryListener);
            searcherComponentFactoryListener = null;
        }
        if (dereferencerComponentFactoryListener != null) {
            context.getBundleContext().removeServiceListener(dereferencerComponentFactoryListener);
            dereferencerComponentFactoryListener = null;
        }
        if (cacheTracker != null) {
            cacheTracker.close();
            cacheTracker = null;
        }
        this.fieldMappings = null;
        this.context = null;
        this.siteConfiguration = null;
    }

    /*
     * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Method for handling the
     * OfflineMode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     */

    /**
     * Called by the ConfigurationAdmin to bind the {@link #offlineMode} if the service becomes available
     * 
     * @param mode
     */
    protected final void enableOfflineMode(OfflineMode mode) {
        this.offlineMode = mode;
    }

    /**
     * Called by the ConfigurationAdmin to unbind the {@link #offlineMode} if the service becomes unavailable
     * 
     * @param mode
     */
    protected final void disableOfflineMode(OfflineMode mode) {
        this.offlineMode = null;
    }

    /**
     * Returns <code>true</code> only if Stanbol operates in {@link OfflineMode} .
     * 
     * @return the offline state
     */
    protected final boolean isOfflineMode() {
        return offlineMode != null;
    }

    /**
     * Basically this Method throws an {@link SiteException} in case Stanbol operates in offline mode
     * 
     * @param uri
     *            the URI of the remote service
     * @param clazz
     *            the clazz of the service that would like to refer the remote service
     * @throws SiteException
     *             in case {@link #isOfflineMode()} returns <code>true</code>
     */
    private void ensureOnline(String uri, Class<?> clazz) throws SiteException {
        if (isOfflineMode()) {
            throw new SiteException(String.format(
                "Unable to access remote Service %s by using %s because Stanbol runs in OfflineMode", uri,
                clazz.getSimpleName()));
        }
    }
}
