/*******************************************************************************
 * Copyright (c) 2013, 2014 Oracle. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *      gonural - initial implementation
 ******************************************************************************/
package org.eclipse.persistence.jpa.rs.exceptions;

import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.jaxb.JAXBContext;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.eclipse.persistence.jpa.rs.DataStorage;
import org.eclipse.persistence.jpa.rs.features.ServiceVersion;
import org.eclipse.persistence.jpa.rs.util.JPARSLogger;
import org.eclipse.persistence.jpa.rs.util.StreamingOutputMarshaller;

import javax.naming.NamingException;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.persistence.PessimisticLockException;
import javax.persistence.QueryTimeoutException;
import javax.persistence.RollbackException;
import javax.persistence.TransactionRequiredException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractExceptionMapper {
    @Context
    private HttpHeaders headers;

    @Context
    UriInfo uriInfo;

    private static final Map<String, Status> HTTP_STATUS_CODE_MAPPING =
            Collections.unmodifiableMap(new HashMap<String, Status>() {
                {
                    put(ClassNotFoundException.class.getName(), Status.BAD_REQUEST);
                    put(ConversionException.class.getName(), Status.BAD_REQUEST);
                    put(DatabaseException.class.getName(), Status.INTERNAL_SERVER_ERROR);
                    put(EntityExistsException.class.getName(), Status.CONFLICT);
                    put(EntityNotFoundException.class.getName(), Status.NOT_FOUND);
                    put(IOException.class.getName(), Status.BAD_REQUEST);
                    put(IllegalAccessException.class.getName(), Status.BAD_REQUEST);
                    put(IllegalArgumentException.class.getName(), Status.BAD_REQUEST);
                    put(IllegalStateException.class.getName(), Status.BAD_REQUEST);
                    put(InvocationTargetException.class.getName(), Status.INTERNAL_SERVER_ERROR);
                    put(JAXBException.class.getName(), Status.NOT_FOUND); // TODO: we might want to change this http status in v2.0
                    put(MalformedURLException.class.getName(), Status.BAD_REQUEST);
                    put(NamingException.class.getName(), Status.BAD_REQUEST);
                    put(NoResultException.class.getName(), Status.NOT_FOUND);
                    put(NoSuchMethodException.class.getName(), Status.BAD_REQUEST);
                    put(NonUniqueResultException.class.getName(), Status.NOT_FOUND);
                    put(NumberFormatException.class.getName(), Status.BAD_REQUEST);
                    put(OptimisticLockException.class.getName(), Status.INTERNAL_SERVER_ERROR);
                    put(PersistenceException.class.getName(), Status.INTERNAL_SERVER_ERROR);
                    put(PessimisticLockException.class.getName(), Status.INTERNAL_SERVER_ERROR);
                    put(QueryTimeoutException.class.getName(), Status.BAD_REQUEST);// TODO: we might want to change this http status in v2.0
                    put(RollbackException.class.getName(), Status.BAD_REQUEST);
                    put(TransactionRequiredException.class.getName(), Status.INTERNAL_SERVER_ERROR);
                }
            });

    // An absolute URI that identifies the problem type.  When dereferenced, it SHOULD provide human-readable documentation for the problem type (e.g., using HTML)."
    private static final String PROBLEM_TYPE = "http://www.eclipse.org/eclipselink/documentation/";

    protected Response buildResponse(JPARSException exception) {
        String path = null;
        exception.setHttpStatusCode(getHttpStatusCode(exception.getCause()));

        if (uriInfo != null) {
            URI requestURI = uriInfo.getRequestUri();
            if (requestURI != null) {
                path = requestURI.getPath();
            }
        }

        if ((path != null) && (path.contains(ServiceVersion.VERSION_2_0.getCode()) || path.contains(ServiceVersion.LATEST.getCode()))) {
            ErrorResponse errorResponse = new ErrorResponse(PROBLEM_TYPE, exception.getMessage(), String.valueOf(exception.getErrorCode()));
            errorResponse.setRequestId((String) DataStorage.get(DataStorage.REQUEST_ID));
            errorResponse.setHttpStatus(exception.getHttpStatusCode().getStatusCode());

            String error = marshallErrorResponse(errorResponse, StreamingOutputMarshaller.getResponseMediaType(headers).toString());
            if (error != null) {
                return Response.status(exception.getHttpStatusCode()).entity(error).type(StreamingOutputMarshaller.getResponseMediaType(headers)).build();
            }
        }
        return Response.status(exception.getHttpStatusCode()).type(StreamingOutputMarshaller.getResponseMediaType(headers)).build();
    }

    private static Status getHttpStatusCode(Throwable throwable) {
        if (throwable != null) {
            Status httpStatusCode = HTTP_STATUS_CODE_MAPPING.get(throwable.getClass().getName());
            if (throwable instanceof RollbackException) {
                Throwable cause = throwable.getCause();
                if (cause != null) {
                    if (cause instanceof DatabaseException) {
                        //  409 Conflict ("The request could not be completed due to a conflict with the current state of the resource.")
                        httpStatusCode = Status.CONFLICT;
                    }
                }
            }
            if (httpStatusCode != null) {
                return httpStatusCode;
            }
        }
        return Status.BAD_REQUEST;
    }

    private String marshallErrorResponse(ErrorResponse errorResponse, String mediaType) {
        try {
            JAXBContext context = (JAXBContext) JAXBContextFactory.createContext(new Class[] { ErrorResponse.class }, null);
            Marshaller marshaller = context.createMarshaller();

            marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, mediaType);
            marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, Boolean.FALSE);
            marshaller.setProperty(MarshallerProperties.JSON_REDUCE_ANY_ARRAYS, true);

            StringWriter writer = new StringWriter();
            marshaller.marshal(errorResponse, writer);
            return writer.toString();
        } catch (Exception ex) {
            JPARSLogger.exception(ex.getMessage(), null, ex);
        }
        return null;
    }
}
