/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */

package org.dspace.rdf;

import java.sql.SQLException;
import java.util.List;
import java.util.UUID;

import com.hp.hpl.jena.rdf.model.Model;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.Site;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.rdf.factory.RDFFactory;
import org.dspace.services.factory.DSpaceServicesFactory;

/**
 * @author Pascal-Nicolas Becker (dspace -at- pascal -hyphen- becker -dot- de)
 */
public class RDFUtil {
    private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(RDFUtil.class);
    private static final AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance()
                                                                                    .getAuthorizeService();

    public static final String CONTENT_NEGOTIATION_KEY = "rdf.contentNegotiation.enable";
    /**
     * Key of the Property to load the types of DSpaceObjects that should get
     * converted.
     */
    public static final String CONVERTER_DSOTYPES_KEY = "rdf.converter.DSOtypes";
    /**
     * Property key to load the password if authentication for the graph store
     * endpoint is required.
     */
    public static final String STORAGE_GRAPHSTORE_PASSWORD_KEY = "rdf.storage.graphstore.password";
    /**
     * Property key to load the URL of the dspace-rdf module. This is necessary
     * to create links from the UI to RDF representation of
     * DSpaceObjects.
     */
    public static final String CONTEXT_PATH_KEY = "rdf.contextPath";
    /**
     * Property key to load the public address of the SPARQL endpoint.
     */
    public static final String SPARQL_ENDPOINT_KEY = "rdf.public.sparql.endpoint";
    /**
     * Property key to load the username if authentication for the internal
     * SPARQL endpoint is required.
     */
    public static final String STORAGE_SPARQL_LOGIN_KEY = "rdf.storage.sparql.login";
    /**
     * Property key to load the password if authentication for the internal
     * SPARQL endpoint is required.
     */
    public static final String STORAGE_SPARQL_PASSWORD_KEY = "rdf.storage.sparql.password";
    /**
     * Property key to load the address of the SPARQL 1.1 GRAPH STORE HTTP
     * PROTOCOL endpoint.
     */
    public static final String STORAGE_GRAPHSTORE_ENDPOINT_KEY = "rdf.storage.graphstore.endpoint";
    /**
     * Property key to load the address of the SPARQL endpoint to use within
     * DSpace. If the property is empty or does not exist, the public SPARQL
     * endpoint will be used.
     */
    public static final String STORAGE_SPARQL_ENDPOINT_KEY = "rdf.storage.sparql.endpoint";
    /**
     * Property key to load the username if authentication for the graph store
     * endpoint is required.
     */
    public static final String STORAGE_GRAPHSTORE_LOGIN_KEY = "rdf.storage.graphstore.login";

    /**
     * Default constructor
     */
    private RDFUtil() { }

    /**
     * Loads converted data of a DSpaceObject identified by the URI provided
     * as {@code identifier}. This method uses the RDFStorage configurated in
     * the DSpace configuration.  Close the model
     * ({@link com.hp.hpl.jena.rdf.model.Model#close() Model.close()}) as soon
     * as possible to free system resources.
     *
     * @param identifier A URI representing the object you want to load data about.
     * @return A model containing the RDF data to the specified identifier or
     * null if no data could be found.
     */
    public static Model loadModel(String identifier) {
        return RDFFactory.getInstance().getRDFStorage().load(identifier);
    }

    /**
     * Generates a URI identifying the provided DSpaceObject. This method
     * automatically loads and instantiates the URIGenerator configured in
     * DSpace configuration.
     * Please note that URIs can be generated for DSpaceObjects of the
     * type SITE, COMMUNITY, COLLECTION or ITEM only. Currently dspace-rdf
     * doesn't support Bundles or Bitstreams as independent entity.
     *
     * @param context DSpace Context.
     * @param dso     DSpace Object you want to get an identifier for.
     * @return URI to identify the DSO or null if no URI could be generated.
     * This can happen f.e. if you use a URIGenerator that uses
     * persistent identifier like DOIs or Handles but there is no such
     * identifier assigned to the provided DSO.
     * @throws SQLException An exception that provides information on a database access error or other errors.
     */
    public static String generateIdentifier(Context context, DSpaceObject dso)
        throws SQLException {
        return RDFFactory.getInstance().getURIGenerator().generateIdentifier(context, dso);
    }

    /**
     * Generates a URI identifying the provided DSpaceObject. This method
     * automatically loads and instantiates the URIGenerator configured in
     * DSpace configuration.
     * Please note that URIs can be generated for DSpaceObjects of the
     * type SITE, COMMUNITY, COLLECTION or ITEM only. Currently dspace-rdf
     * doesn't support Bundles or Bitstreams as independent entity.
     *
     * @param context    DSpace Context.
     * @param type       Type of the DSpaceObject you want to generate a URI for (e.g.
     *                   {@link org.dspace.core.Constants#ITEM Constants.ITEM}.
     * @param id         UUID of the DSpaceObject you want to generate a URI for.
     * @param handle     Handle of the DSpaceObject you want to generate a URI for.
     * @param identifier identifiers of the object.
     * @return URI to identify the DSO or null if no URI could be generated.
     * This can happen f.e. if you use a URIGenerator that uses
     * persistent identifier like DOIs or Handles but there is no such
     * identifier assigned to the provided DSO.
     * @throws SQLException An exception that provides information on a database access error or other errors.
     */
    public static String generateIdentifier(Context context, int type, UUID id,
                                            String handle, List<String> identifier)
        throws SQLException {
        return RDFFactory.getInstance().getURIGenerator().generateIdentifier(context,
                                                                             type, id, handle, identifier);
    }

    /**
     * Converts the the provided DSpaceObject into RDF and returns the model.
     * Please note that dspace-rdf doesn't support Bundles or Bitstreams as
     * independent entity. You can convert DSpaceObjects of type SITE,
     * COMMUNITY, COLLECTION or ITEM.
     *
     * @param context Consider that the converted data will be stored in a
     *                triple store, that is outside the range of the DSpace
     *                authorization mechanism. Unless you are really sure what
     *                you are doing, you should provide the context of an
     *                anonymous user here, as the triple store probably provides
     *                a public SPARQL endpoint.
     * @param dso     DSpaceObject to convert.
     * @return The converted data or null if the conversion result is empty.
     * Remember to close the model as soon as you don't need it anymore.
     * @throws RDFMissingIdentifierException If no identifier could be generated.
     * @throws SQLException                  if database error
     * @throws ItemNotArchivedException      If you want to convert an Item that is
     *                                       not archived.
     * @throws ItemWithdrawnException        If you want to convert an Item that is
     *                                       withdrawn.
     * @throws ItemNotDiscoverableException  If you want to convert an Item that
     *                                       is not discoverable.
     * @throws AuthorizeException            If the DSpaceObject does not have READ
     *                                       permissions with the provided context.
     * @throws IllegalArgumentException      If the DSpaceObject is not of type SITE,
     *                                       COMMUNITY, COLLECTION or ITEM.
     */
    public static Model convert(Context context, DSpaceObject dso)
        throws RDFMissingIdentifierException, SQLException, ItemNotArchivedException,
        ItemWithdrawnException, ItemNotDiscoverableException,
        AuthorizeException, IllegalArgumentException {
        String[] dsoTypes = DSpaceServicesFactory.getInstance()
                                                 .getConfigurationService()
                                                 .getArrayProperty(CONVERTER_DSOTYPES_KEY);
        if (dsoTypes == null || dsoTypes.length == 0) {
            log.warn("Property rdf." + CONVERTER_DSOTYPES_KEY + " was not found "
                         + "or is empty. Will convert all type of DSpace Objects.");
        } else {
            boolean found = false;
            for (String type : dsoTypes) {
                if (StringUtils.equalsIgnoreCase(Constants.typeText[dso.getType()], type.trim())) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                log.warn("Configuration of DSpaceObjects of type "
                             + Constants.typeText[dso.getType()]
                             + " prohibitted by configuration.");
                return null;
            }
        }
        isPublic(context, dso);
        return RDFFactory.getInstance().getRDFConverter().convert(context, dso);
    }

    /**
     * Converts a DSpaceObject into RDF data and stores them using the configured
     * {@link org.dspace.rdf.storage.RDFStorage RDFStorage}.
     * Please note that dspace-rdf doesn't support Bundles or Bitstreams as
     * independent entity. You can convert DSpaceObjects of type SITE,
     * COMMUNITY, COLLECTION or ITEM.
     *
     * @param context Consider that the converted data will be stored in a
     *                triple store, that is outside the range of the DSpace
     *                authorization mechanism. Unless you are really sure what
     *                you are doing, you should provide the context of an
     *                anonymous user here, as the triple store probably provides
     *                a public SPARQL endpoint.
     * @param dso     DSpaceObject to convert.
     * @return The converted data or null if the conversion result is empty.
     * Remember to close the model as soon as you don't need it anymore.
     * @throws RDFMissingIdentifierException If no identifier could be generated.
     * @throws SQLException                  if database error
     * @throws ItemNotArchivedException      If you want to convert an Item that is
     *                                       not archived.
     * @throws ItemWithdrawnException        If you want to convert an Item that is
     *                                       withdrawn.
     * @throws ItemNotDiscoverableException  If you want to convert an Item that
     *                                       is not discoverable.
     * @throws AuthorizeException            If the DSpaceObject does not have READ
     *                                       permissions with the provided context.
     * @throws IllegalArgumentException      If the DSpaceObject is not of type SITE,
     *                                       COMMUNITY, COLLECTION or ITEM.
     */
    public static Model convertAndStore(Context context, DSpaceObject dso)
        throws RDFMissingIdentifierException, SQLException, ItemNotArchivedException,
        ItemWithdrawnException, ItemNotDiscoverableException,
        AuthorizeException, IllegalArgumentException {
        Model convertedData = convert(context, dso);

        String identifier = generateIdentifier(context, dso);
        if (StringUtils.isEmpty(identifier)) {
            log.error("Cannot generate identifier for dso from type "
                          + ContentServiceFactory.getInstance().getDSpaceObjectService(dso)
                                                 .getTypeText(dso) + " (id: " + dso.getID() + ").");
            if (convertedData != null) {
                convertedData.close();
            }
            throw new RDFMissingIdentifierException(dso.getType(), dso.getID());
        }

        if (convertedData == null) {
            // if data about this dso is stored in the triplestore already, we
            // should remove it as a conversion currently result in no data
            RDFFactory.getInstance().getRDFStorage().delete(identifier);
            return null;
        }

        RDFFactory.getInstance().getRDFStorage().store(identifier, convertedData);
        return convertedData;
    }

    /**
     * Checks whether the provided DSpaceObject is readable within the provided
     * context and if the DSO is an Item whether it is archived, discoverable
     * and not withdrawn.
     *
     * @param context Consider that the converted data will be stored in a
     *                triple store, that is outside the range of the DSpace
     *                authorization mechanism. Unless you are really sure what
     *                you are doing, you should provide the context of an
     *                anonymous user here, as the triple store probably provides
     *                a public SPARQL endpoint.
     * @param dso     The DSpaceObjet to check.
     * @throws SQLException                 if database error
     * @throws ItemNotArchivedException     If {@code dso} is an Item and is not
     *                                      archived.
     * @throws ItemWithdrawnException       If {@code dso} is an Item and is withdrawn.
     * @throws ItemNotDiscoverableException If {@code dso} is an Item and is not
     *                                      discoverable.
     * @throws AuthorizeException           If {@code context} does not grant {@code READ}
     *                                      permissions for {@code dso}.
     */
    public static void isPublic(Context context, DSpaceObject dso)
        throws SQLException, ItemNotArchivedException, ItemWithdrawnException,
        ItemNotDiscoverableException, AuthorizeException {
        // as there is no way to set site permissions in UI, we
        // ignore the permissions of the repository root (DSpaceObject of type
        // Site).
        if (dso instanceof Site) {
            return;
        }
        authorizeService.authorizeAction(context, dso, Constants.READ);
        if (dso instanceof Item) {
            Item item = (Item) dso;
            if (!item.isArchived()) {
                throw new ItemNotArchivedException();
            }
            if (!item.isDiscoverable()) {
                throw new ItemNotDiscoverableException();
            }
            if (item.isWithdrawn()) {
                throw new ItemWithdrawnException();
            }
        }
    }

    /**
     * Does the same as {@link #isPublic(Context, DSpaceObject)
     * isPublic(Context, DSpaceObject)} but returns a boolean instead of throwing
     * exceptions. For those who don't want to deal with catching exceptions.
     *
     * @param context Consider that the converted data will be stored in a
     *                triple store, that is outside the range of the DSpace
     *                authorization mechanism. Unless you are really sure what
     *                you are doing, you should provide the context of an
     *                anonymous user here, as the triple store probably provides
     *                a public SPARQL endpoint.
     * @param dso     The DSpaceObjet to check.
     * @return true if {@link #isPublic(Context, DSpaceObject)
     * isPublic(Context, DSpaceObject)} doesn't throw an exception, false if it
     * did.
     * @throws SQLException if database error
     */
    public static boolean isPublicBoolean(Context context, DSpaceObject dso)
        throws SQLException {
        try {
            RDFUtil.isPublic(context, dso);
        } catch (ItemNotArchivedException | ItemWithdrawnException
            | ItemNotDiscoverableException | AuthorizeException ex) {
            return false;
        }
        return true;
    }

    /**
     * Deletes the data identified by the URI from the triple store.
     *
     * @param uri URI to identify the named graph to delete.
     */
    public static void delete(String uri) {
        RDFFactory.getInstance().getRDFStorage().delete(uri);
    }

    /**
     * This is a shortcut to generate an RDF identifier for a DSpaceObject and
     * to delete the identified data from the named graph.
     *
     * @param ctx         The relevant DSpace Context.
     * @param type        DSpaceObject type (e.g. {@link Constants#ITEM Constants.ITEM}).
     * @param id          Id of the DspaceObject.
     * @param handle      Handle of the DSpaceObject.
     * @param identifiers list of identifiers
     * @throws SQLException                  if database error
     * @throws RDFMissingIdentifierException In case that no Identifier could be generated.
     */
    public static void delete(Context ctx, int type, UUID id, String handle, List<String> identifiers)
        throws SQLException, RDFMissingIdentifierException {
        String uri = RDFFactory.getInstance().getURIGenerator()
                               .generateIdentifier(ctx, type, id, handle, identifiers);
        if (uri != null) {
            RDFFactory.getInstance().getRDFStorage().delete(uri);
        } else {
            throw new RDFMissingIdentifierException(type, id);
        }
    }

}
