/*
 * 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.jena.fuseki.servlets;

import static org.apache.jena.fuseki.servlets.GraphTarget.determineTargetGSP;
import static org.apache.jena.riot.WebContent.ctMultipartMixed;
import static org.apache.jena.riot.WebContent.matchContentType;

import org.apache.jena.atlas.web.ContentType;
import org.apache.jena.fuseki.FusekiConfigException;
import org.apache.jena.fuseki.server.Validators;
import org.apache.jena.fuseki.system.FusekiNetLib;
import org.apache.jena.fuseki.system.Upload;
import org.apache.jena.fuseki.system.UploadDetails;
import org.apache.jena.graph.Graph;
import org.apache.jena.riot.RiotException;
import org.apache.jena.riot.RiotParseException;
import org.apache.jena.riot.system.StreamRDF;
import org.apache.jena.riot.system.StreamRDFLib;
import org.apache.jena.riot.web.HttpNames;
import org.apache.jena.shared.OperationDeniedException;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.DatasetGraphFactory;
import org.apache.jena.sparql.graph.GraphFactory;
import org.apache.jena.web.HttpSC;

public class GSP_RW extends GSP_R {

    public GSP_RW() {}

    @Override
    protected void doOptions(HttpAction action) {
        ActionLib.setCommonHeadersForOptions(action);
        if ( GSPLib.hasGSPParams(action) )
            action.setResponseHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,PUT,DELETE,POST");
        else
            action.setResponseHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,PUT,POST");
        ServletOps.success(action);
    }

    @Override
    protected void doDelete(HttpAction action) {
        if ( isQuads(action) )
            execDeleteQuads(action);
        else
            execDeleteGSP(action);
   }

    @Override
    protected void doPut(HttpAction action) {
        if ( isQuads(action) )
            execPutQuads(action);
        else
            execPutGSP(action);
    }

    @Override
    protected void doPost(HttpAction action) {
        if ( isQuads(action) )
            execPostQuads(action);
        else
            execPostGSP(action);
    }

    protected void execPostGSP(HttpAction action) { doPutPostGSP(action, false); }

    protected void execPostQuads(HttpAction action) { doPutPostQuads(action, false); }

    protected void execPutGSP(HttpAction action) { doPutPostGSP(action, true); }

    protected void execPutQuads(HttpAction action) { doPutPostQuads(action, true); }

    protected void execDeleteGSP(HttpAction action) {
        action.beginWrite();
        boolean haveCommited = false;
        try {
            DatasetGraph dsg = decideDataset(action);
            GraphTarget target = determineTargetGSP(dsg, action);
            if ( action.log.isDebugEnabled() )
                action.log.debug("DELETE->"+target);
            if ( target.isUnion() )
                ServletOps.errorBadRequest("Can't delete the union graph");
            boolean existedBefore = target.exists();
            if ( !existedBefore ) {
                // Commit, not abort, because locking "transactions" don't support abort.
                action.commit();
                haveCommited = true;
                ServletOps.errorNotFound("No such graph: "+target.label());
            }
            deleteGraph(dsg, action);
            action.commit();
            haveCommited = true;
        }
        catch (ActionErrorException ex) { throw ex; }
        catch (Exception ex) { abortSilent(action); }
        finally { action.end(); }
        ServletOps.successNoContent(action);
    }

    protected void execDeleteQuads(HttpAction action) {
        // Don't allow whole-database DELETE.
        ServletOps.errorMethodNotAllowed("DELETE");
    }

    protected void doPutPostGSP(HttpAction action, boolean overwrite) {
        ContentType ct = ActionLib.getContentType(action);
        if ( ct == null )
            ServletOps.errorBadRequest("No Content-Type:");

        if ( matchContentType(ctMultipartMixed, ct) ) {
            ServletOps.error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "multipart/mixed not supported");
        }

        UploadDetails details;
        if ( action.isTransactional() )
            details = addDataIntoTxn(action, overwrite);
        else
            details = addDataIntoNonTxn(action, overwrite);

        ServletOps.uploadResponse(action, details);
    }

    // Refactoring:
    //   Make doPutPost and doPutPostQuads the same pattern.
    //   addDataInto*: Extract commonality in error handling.

    /** Directly add data in a transaction.
     * Assumes recovery from parse errors by transaction abort.
     * Return whether the target existed before.
     * @param action
     * @param cleanDest Whether to remove data first (true = PUT, false = POST)
     * @return whether the target existed beforehand
     */
    private UploadDetails addDataIntoTxn(HttpAction action, boolean overwrite) {
        action.beginWrite();
        try {
            DatasetGraph dsg = decideDataset(action);
            GraphTarget target = determineTargetGSP(dsg, action);
            if ( action.log.isDebugEnabled() )
                action.log.debug(action.getRequestMethod().toUpperCase()+"->"+target);
            if ( target.isUnion() )
                ServletOps.errorBadRequest("Can't delete the union graph");
            // Check URI.
            if ( ! target.isDefault() && target.graphName() != null && ! target.graphName().isBlank()) {
                String uri = target.graphName().getURI();
                try {
                    Validators.graphName(uri);
                } catch (FusekiConfigException ex) {
                    ServletOps.errorBadRequest("Bad URI: "+uri);
                    return null;
                }
            }

            boolean existedBefore = target.exists();
            Graph g = target.graph();
            if ( overwrite && existedBefore )
                clearGraph(target);
            StreamRDF sink = StreamRDFLib.graph(g);
            UploadDetails upload = Upload.incomingData(action, sink);
            upload.setExistedBefore(existedBefore);
            action.commit();
            return upload;
        } catch (RiotParseException ex) {
            abortSilent(action);
            ServletOps.errorParseError(ex);
            return null;
        } catch (RiotException ex) {
            // Parse error
            abortSilent(action);
            ServletOps.errorBadRequest(ex.getMessage());
            return null;
        } catch (OperationDeniedException ex) {
            abortSilent(action);
            throw ex;
        } catch (ActionErrorException ex) {
            // Any ServletOps.error from calls in the try{} block.
            abortSilent(action);
            throw ex;
        } catch (Exception ex) {
            // Something unexpected.
            abortSilent(action);
            ServletOps.errorOccurred(ex.getMessage());
            return null;
        } finally {
            action.end();
        }
    }

    private static void abortSilent(HttpAction action) {
        action.abortSilent();
    }

    /** Add data where the destination does not support full transactions.
     *  In particular, with no abort, and actions probably going to the real storage
     *  parse errors can lead to partial updates.  Instead, parse to a temporary
     *  graph, then insert that data.
     * @param action
     * @param cleanDest Whether to remove data first (true = PUT, false = POST)
     * @return whether the target existed beforehand.
     */
    private UploadDetails addDataIntoNonTxn(HttpAction action, boolean overwrite) {
        Graph graphTmp = GraphFactory.createGraphMem();
        StreamRDF dest = StreamRDFLib.graph(graphTmp);

        UploadDetails details;
        try { details = Upload.incomingData(action, dest); }
        catch (RiotParseException ex) {
            ServletOps.errorParseError(ex);
            return null;
        }
        // Now insert into dataset
        action.beginWrite();
        try {
            DatasetGraph dsg = decideDataset(action);
            GraphTarget target = determineTargetGSP(dsg, action);
            if ( action.log.isDebugEnabled() )
                action.log.debug("  ->"+target);
            if ( target.isUnion() )
                ServletOps.errorBadRequest("Can't delete the union graph");
            boolean existedBefore = target.exists();
            if ( overwrite && existedBefore )
                clearGraph(target);
            FusekiNetLib.addDataInto(graphTmp, target.dataset(), target.graphName());
            details.setExistedBefore(existedBefore);
            action.commit();
            return details;
        } catch (OperationDeniedException ex) {
            abortSilent(action);
            throw ex;
        } catch (Exception ex) {
            // We parsed into a temporary graph so an exception at this point
            // is not because of a parse error.
            // We're in the non-transactional branch, this probably will not work
            // but it might and there is no harm safely trying.
            abortSilent(action);
            ServletOps.errorOccurred(ex.getMessage());
            return null;
        } finally { action.end(); }
    }

    /** Delete a graph. This removes the storage choice and looses the setup.
     * The default graph is cleared, not removed.
     */
    protected static void deleteGraph(DatasetGraph dsg, HttpAction action) {
        GraphTarget target = determineTargetGSP(dsg, action);
        if ( target.isDefault() )
            clearGraph(target);
        else
            target.dataset().removeGraph(target.graphName());
    }

    /** Clear a graph - this leaves the storage choice and setup in-place */
    protected static void clearGraph(GraphTarget target) {
        Graph g = target.graph();
        g.getPrefixMapping().clearNsPrefixMap();
        g.clear();
    }

    // ---- Quads

    protected void doPutPostQuads(HttpAction action, boolean overwrite) {
        // See doPutPostGSP
        if ( !action.getDataService().allowUpdate() )
            ServletOps.errorMethodNotAllowed(action.getMethod());
        if ( action.isTransactional() )
            quadsPutPostTxn(action, overwrite);
        else
            quadsPutPostNonTxn(action, overwrite);
    }

    /**
     * Load data using a transaction into the dataset of an action. if the data is bad,
     * abort the transaction.
     */
    private void quadsPutPostTxn(HttpAction action, boolean clearFirst) {
        UploadDetails details = null;
        action.beginWrite();
        try {
            DatasetGraph dsg = decideDataset(action);
            if ( clearFirst )
                dsg.clear();
            StreamRDF dest = StreamRDFLib.dataset(dsg);
            details = Upload.incomingData(action, dest);
            action.commit();
            ServletOps.success(action);
        } catch (RiotException ex) {
            // Parse error
            abortSilent(action);
            ServletOps.errorBadRequest(ex.getMessage());
        } catch (OperationDeniedException ex) {
            abortSilent(action);
            throw ex;
        } catch (ActionErrorException ex) {
            abortSilent(action);
            throw ex;
        } catch (Exception ex) {
            // Something else went wrong. Backout.
            abortSilent(action);
            ServletOps.errorOccurred(ex.getMessage());
        } finally {
            action.end();
        }
        ServletOps.uploadResponse(action, details);
    }

    /**
     * Load data, without assuming the dataset of an action is transactional -
     * specifically, whether it supports "abort". This requires loading the data into
     * a temporary dataset, which means we check the data is legal RDF, then copying
     * it into the finally destination.
     */
    private void quadsPutPostNonTxn(HttpAction action, boolean clearFirst) {
        DatasetGraph dsgTmp = DatasetGraphFactory.create();
        StreamRDF dest = StreamRDFLib.dataset(dsgTmp);

        UploadDetails details;
        try {
            details = Upload.incomingData(action, dest);
        } catch (RiotException ex) {
            ServletOps.errorBadRequest(ex.getMessage());
            return;
        }
        // Now insert into dataset
        action.beginWrite();
        try {
            DatasetGraph dsg = decideDataset(action);
            if ( clearFirst )
                dsg.clear();
            FusekiNetLib.addDataInto(dsgTmp, dsg);
            action.commit();
            ServletOps.success(action);
        } catch (OperationDeniedException ex) {
            abortSilent(action);
            throw ex;
        } catch (Exception ex) {
            // We're in a non-transactional upload so this probably will not
            // work but there still may be transaction state tracking.
            // There is no harm safely trying.
            abortSilent(action);
            ServletOps.errorOccurred(ex.getMessage());
        } finally {
            action.end();
        }
        ServletOps.uploadResponse(action, details);
    }
}
