/*******************************************************************************
 * Copyright (c) 2011, 2016 Oracle and/or its affiliates. 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
 *      Dmitry Kornilov - JPARS 2.0 related changes
 ******************************************************************************/
package org.eclipse.persistence.jpa.rs.resources.common;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;

import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.jpa.rs.metadata.model.Link;
import org.eclipse.persistence.internal.jpa.rs.metadata.model.Parameter;
import org.eclipse.persistence.internal.jpa.rs.metadata.model.SessionBeanCall;
import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.ContextsCatalog;
import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.Resource;
import org.eclipse.persistence.jaxb.JAXBContext;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.eclipse.persistence.jpa.rs.PersistenceContext;
import org.eclipse.persistence.jpa.rs.exceptions.JPARSException;
import org.eclipse.persistence.jpa.rs.features.ItemLinksBuilder;
import org.eclipse.persistence.jpa.rs.features.ServiceVersion;
import org.eclipse.persistence.jpa.rs.util.HrefHelper;
import org.eclipse.persistence.jpa.rs.util.JPARSLogger;
import org.eclipse.persistence.jpa.rs.util.StreamingOutputMarshaller;
import org.eclipse.persistence.jpa.rs.util.list.LinkList;

/**
 * Base class for persistent unit resources.
 *
 * @author gonural
 */
public abstract class AbstractPersistenceResource extends AbstractResource {
    private static final String CLASS_NAME = AbstractPersistenceResource.class.getName();

    /**
     * Produces a response containing a list of available persistence contexts.
     * Returns different responses depending version.
     *
     * @param version the service version (null, "v1.0", "v2.0", "latest")
     * @param headers the HTTP headers
     * @param uriInfo the URL
     * @return response containing a list of persistence contexts.
     */
    protected Response getContextsInternal(String version, HttpHeaders headers, UriInfo uriInfo) {
        JPARSLogger.entering(CLASS_NAME, "getContextsInternal", new Object[] { "GET", version, uriInfo.getRequestUri().toASCIIString() });
        if (!isValidVersion(version)) {
            JPARSLogger.error("unsupported_service_version_in_the_request", new Object[] { version });
            throw JPARSException.invalidServiceVersion(version);
        }

        if (ServiceVersion.fromCode(version).compareTo(ServiceVersion.VERSION_2_0) >= 0) {
            return getContextsV2(version, headers, uriInfo);
        } else {
            return getContextsV1(version, headers, uriInfo);
        }
    }

    @SuppressWarnings("rawtypes")
    protected Response callSessionBeanInternal(String version, HttpHeaders headers, UriInfo uriInfo, InputStream is) {
        JPARSLogger.entering(CLASS_NAME, "callSessionBeanInternal", new Object[] { "POST", headers.getMediaType(), version, uriInfo.getRequestUri().toASCIIString() });
        try {
            if (!isValidVersion(version)) {
                JPARSLogger.error("unsupported_service_version_in_the_request", new Object[] { version });
                throw JPARSException.invalidServiceVersion(version);
            }

            SessionBeanCall call = unmarshallSessionBeanCall(is);

            String jndiName = call.getJndiName();
            if (!isValid(jndiName)) {
                JPARSLogger.error("jpars_invalid_jndi_name", new Object[] { jndiName });
                throw JPARSException.jndiNamePassedIsInvalid(jndiName);
            }

            javax.naming.Context ctx = new InitialContext();
            Object ans = ctx.lookup(jndiName);
            if (ans == null) {
                JPARSLogger.error("jpars_could_not_find_session_bean", new Object[] { jndiName });
                throw JPARSException.sessionBeanCouldNotBeFound(jndiName);
            }

            PersistenceContext context = null;
            if (call.getContext() != null) {
                context = getPersistenceFactory().get(call.getContext(), uriInfo.getBaseUri(), version, null);
                if (context == null) {
                    JPARSLogger.error("jpars_could_not_find_persistence_context", new Object[] { call.getContext() });
                    throw JPARSException.persistenceContextCouldNotBeBootstrapped(call.getContext());
                }
            }

            Class[] parameters = new Class[call.getParameters().size()];
            Object[] args = new Object[call.getParameters().size()];
            int i = 0;
            for (Parameter param : call.getParameters()) {
                Class parameterClass = null;
                Object parameterValue;
                if (context != null) {
                    parameterClass = context.getClass(param.getTypeName());
                }
                if (parameterClass != null) {
                    parameterValue = context.unmarshalEntity(param.getTypeName(), headers.getMediaType(), is);
                } else {
                    parameterClass = Thread.currentThread().getContextClassLoader().loadClass(param.getTypeName());
                    parameterValue = ConversionManager.getDefaultManager().convertObject(param.getValue(), parameterClass);
                }
                parameters[i] = parameterClass;
                args[i] = parameterValue;
                i++;
            }
            Method method = ans.getClass().getMethod(call.getMethodName(), parameters);
            Object returnValue = method.invoke(ans, args);
            return Response.ok(new StreamingOutputMarshaller(null, returnValue, headers.getAcceptableMediaTypes())).build();
        } catch (JAXBException | NamingException | ReflectiveOperationException | RuntimeException e) {
            JPARSLogger.exception("exception_in_callSessionBeanInternal", new Object[]{version, headers.getMediaType(), uriInfo.getRequestUri().toASCIIString()}, e);
            throw JPARSException.exceptionOccurred(e);
        }
    }

    private boolean isValid(String jndiName) {
        String protocol = null;
        int colon = jndiName.indexOf(':');
        int slash = jndiName.indexOf('/');
        if (colon > 0 && (slash == -1 || colon < slash)) {
            protocol = jndiName.substring(0, colon);
        }
        return protocol == null || protocol.isEmpty() || protocol.equalsIgnoreCase("java") || protocol.equalsIgnoreCase("ejb");
    }

    private SessionBeanCall unmarshallSessionBeanCall(InputStream data) throws JAXBException {
        Class<?>[] jaxbClasses = new Class[] { SessionBeanCall.class };
        JAXBContext context = (JAXBContext) JAXBContextFactory.createContext(jaxbClasses, null);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, Boolean.FALSE);
        unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
        StreamSource ss = new StreamSource(data);
        return unmarshaller.unmarshal(ss, SessionBeanCall.class).getValue();
    }

    /**
     * Returns a response object containing the available contexts list. Used in JPARS 1.0 and below.
     *
     * @param version the version (null or "v1.0")
     * @param headers the HTTP headers
     * @param uriInfo the UTL info
     * @return Response object
     */
    private Response getContextsV1(String version, HttpHeaders headers, UriInfo uriInfo) {
        JPARSLogger.entering(CLASS_NAME, "getContextsV1", new Object[] { "GET", version, uriInfo.getRequestUri().toASCIIString() });
        try {
            final Set<String> contexts = getPersistenceFactory().getPersistenceContextNames();
            final String mediaType = StreamingOutputMarshaller.mediaType(headers.getAcceptableMediaTypes()).toString();
            final URI baseURI = uriInfo.getBaseUri();
            final List<Link> links = new ArrayList<>();

            for (String context : contexts) {
                if (version != null) {
                    links.add(new Link(context, mediaType, baseURI + version + "/" + context + "/metadata"));
                } else {
                    links.add(new Link(context, mediaType, baseURI + context + "/metadata"));
                }
            }

            final LinkList linkList = new LinkList();
            linkList.setList(links);

            final String result;
            if (mediaType.equals(MediaType.APPLICATION_JSON)) {
                result = marshallMetadata(linkList.getList(), mediaType);
            } else {
                result = marshallMetadata(linkList, mediaType);
            }
            return Response.ok(new StreamingOutputMarshaller(null, result, headers.getAcceptableMediaTypes())).build();
        } catch (JAXBException ex) {
            JPARSLogger.exception("exception_in_getContextsV1", new Object[] {version, headers.getMediaType(), uriInfo.getRequestUri().toASCIIString()}, ex);
            throw JPARSException.exceptionOccurred(ex);
        }
    }

    /**
     * Returns a response object containing the available contexts list. Used in JPARS 2.0 or above.
     *
     * @param version the version ("v2.0" or higher)
     * @param headers the HTTP headers
     * @param uriInfo the UTL info
     * @return Response object
     */
    private Response getContextsV2(String version, HttpHeaders headers, UriInfo uriInfo) {
        JPARSLogger.entering(CLASS_NAME, "getContextsV2", new Object[] { "GET", version, uriInfo.getRequestUri().toASCIIString() });
        try {
            final ContextsCatalog result = new ContextsCatalog();

            final Set<String> contexts = getPersistenceFactory().getPersistenceContextNames();
            for (String context : contexts) {
                final Resource contextResource = new Resource();
                contextResource.setName(context);

                final String href = HrefHelper.getRoot(uriInfo.getBaseUri().toString(), version, context).append("/metadata-catalog").toString();
                contextResource.setLinks((new ItemLinksBuilder())
                        .addCanonical(href)
                        .getList());

                result.addContext(contextResource);
            }

            final String mediaType = StreamingOutputMarshaller.mediaType(headers.getAcceptableMediaTypes()).toString();
            final String marshalled = marshallMetadata(result, mediaType);

            return Response.ok(new StreamingOutputMarshaller(null, marshalled, headers.getAcceptableMediaTypes())).build();
        } catch (JAXBException ex) {
            JPARSLogger.exception("exception_in_getContextsV2", new Object[] {version, headers.getMediaType(), uriInfo.getRequestUri().toASCIIString()}, ex);
            throw JPARSException.exceptionOccurred(ex);
        }
    }
}
