/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2015 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.jersey.tests.e2e.server.wadl;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.internal.util.SaxHelper;
import org.glassfish.jersey.internal.util.SimpleNamespaceResolver;
import org.glassfish.jersey.message.internal.MediaTypes;
import org.glassfish.jersey.server.ExtendedResourceContext;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.ExtendedResource;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.server.wadl.internal.WadlUtils;
import org.glassfish.jersey.test.JerseyTest;

import org.junit.Test;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import static org.junit.Assert.assertEquals;

import jersey.repackaged.com.google.common.collect.Maps;

/**
 * Test verifies functionality of {@link org.glassfish.jersey.server.model.Resource#isExtended()} and its
 * influence on WADL.
 *
 * @author Miroslav Fuksa
 */
public class ResourceExtendedFlagTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new ResourceConfig(MyResource.class, AllExtended.class, LoggingFilter.class);
    }

    @Path("all-extended")
    public static class AllExtended {

        @GET
        @ExtendedResource
        public String allExtendedGet() {
            return "get";
        }

        @GET
        @Path("sub")
        @ExtendedResource
        public String allExtendedSubGet() {
            return "sub-get";
        }

        @Path("locator")
        @ExtendedResource
        public SubResourceLocator allExtendedSubLocator() {
            return new SubResourceLocator();
        }
    }

    public static class SubResourceLocator {

        @GET
        public String get() {
            return "get";
        }
    }

    @Path("resource")
    public static class MyResource {

        @Context
        private ExtendedResourceContext extendedResourceContext;

        @GET
        public String resourceGet() {

            final String error = validateModel();
            return error == null ? "ok" : error;
        }

        @GET
        @Path("extended")
        @ExtendedResource
        public String resourceExtendedGet() {
            return "extended";
        }

        @POST
        @ExtendedResource
        public String resourceExtendedPost() {
            return "extendedpost";
        }

        @POST
        @Path("visible")
        public String resourceVisiblePost() {
            return "visiblepost";
        }

        private String validateModel() {
            Map<String, Boolean> extendedMethods = Maps.newHashMap();
            extendedMethods.put("resourceExtendedGet", false);
            extendedMethods.put("resourceExtendedPost", false);
            extendedMethods.put("allExtendedGet", false);
            extendedMethods.put("allExtendedSubGet", false);
            extendedMethods.put("allExtendedSubLocator", false);

            Map<String, Boolean> visibleMethods = Maps.newHashMap();
            visibleMethods.put("resourceVisiblePost", false);
            visibleMethods.put("resourceGet", false);

            final ResourceModel resourceModel = extendedResourceContext.getResourceModel();
            for (Resource rootResource : resourceModel.getRootResources()) {
                final String error = checkResource(rootResource, extendedMethods, visibleMethods, "");
                if (error != null) {
                    return error;
                }
            }

            for (Map.Entry<String, Boolean> entry : extendedMethods.entrySet()) {
                if (entry.getValue() != null && !entry.getValue()) {
                    return "Extended method " + entry.getKey() + " was not found";
                }
            }

            for (Map.Entry<String, Boolean> entry : visibleMethods.entrySet()) {
                if (entry.getValue() != null && !entry.getValue()) {
                    return "Visible method " + entry.getKey() + " was not found";
                }
            }
            return null;
        }

        private String checkResource(Resource resource, Map<String, Boolean> extendedMethods,
                                     Map<String, Boolean> visibleMethods, String prefix) {

            System.out.println(prefix + "R extended=" + resource.isExtended() + " resource: " + resource);

            boolean allExtended = true;
            for (ResourceMethod resourceMethod : resource.getAllMethods()) {
                if ("OPTIONS".equals(resourceMethod.getHttpMethod()) && !resourceMethod.isExtended()) {
                    return "OPTIONS method " + resourceMethod + " is not extended";
                }
                if (!resourceMethod.isExtended()) {
                    allExtended = false;
                }

                final String methodName = resourceMethod.getInvocable().getHandlingMethod().getName();
                if (extendedMethods.get(methodName) != null) {
                    extendedMethods.put(methodName, true);
                    if (!resourceMethod.isExtended()) {
                        return "Method " + methodName + " should be extended.";
                    }
                }

                if (visibleMethods.get(methodName) != null) {
                    visibleMethods.put(methodName, true);
                    if (resourceMethod.isExtended()) {
                        return "Method " + methodName + " should NOT be extended.";
                    }
                }
                System.out.println(prefix + "   M extended=" + resourceMethod.isExtended() + " method: " + methodName);
            }

            for (Resource child : resource.getChildResources()) {
                final String error = checkResource(child, extendedMethods, visibleMethods, "     ");
                if (error != null) {
                    return error;
                }
            }

            if (allExtended != resource.isExtended()) {
                return "Resource " + resource + "Resource.extended = " + resource.isExtended() + " and allExtended="
                        + allExtended;
            }
            return null;
        }
    }

    /**
     * Invokes resource method which goes trough the {@link org.glassfish.jersey.server.model.ResourceModel}
     * and checks whether {@code extended} flag is correctly defined on resources and methods.
     */
    @Test
    public void testResourceModel() {
        final Response response = target().path("resource").request().get();
        assertEquals(200, response.getStatus());
        assertEquals("ok", response.readEntity(String.class));
    }

    /**
     * Tests full wadl with all "extended" details.
     *
     * @throws ParserConfigurationException
     * @throws XPathExpressionException
     * @throws IOException
     * @throws SAXException
     */
    @Test
    public void testDetailedWadl() throws ParserConfigurationException, XPathExpressionException, IOException,
            SAXException {
        Response response = target("/application.wadl").queryParam(WadlUtils.DETAILED_WADL_QUERY_PARAM, "true")
                .request(MediaTypes.WADL_TYPE).get();
        assertEquals(200, response.getStatus());
        File tmpFile = response.readEntity(File.class);
        DocumentBuilderFactory bf = DocumentBuilderFactory.newInstance();
        bf.setNamespaceAware(true);
        bf.setValidating(false);
        if (!SaxHelper.isXdkDocumentBuilderFactory(bf)) {
            bf.setXIncludeAware(false);
        }
        DocumentBuilder b = bf.newDocumentBuilder();
        Document d = b.parse(tmpFile);
        printSource(new DOMSource(d));
        XPath xp = XPathFactory.newInstance().newXPath();
        xp.setNamespaceContext(new SimpleNamespaceResolver("wadl", "http://wadl.dev.java.net/2009/02"));

        // check base URI
        String val = (String) xp.evaluate("/wadl:application/wadl:resources/@base", d, XPathConstants.STRING);
        assertEquals(val, getBaseUri().toString());
        // check total number of resources is 8
        val = (String) xp.evaluate("count(//wadl:resource)", d, XPathConstants.STRING);
        assertEquals("8", val);

        val = (String) xp.evaluate("count(//wadl:resource[@path='all-extended'])", d, XPathConstants.STRING);
        assertEquals("1", val);

        val = (String) xp.evaluate("count(//wadl:resource[@path='resource'])", d, XPathConstants.STRING);
        assertEquals("1", val);

        val = (String) xp.evaluate("count(//wadl:resource[@path='application.wadl'])", d, XPathConstants.STRING);
        assertEquals("1", val);

        xp.setNamespaceContext(new SimpleNamespaceResolver("jersey", "http://jersey.java.net/"));
        val = (String) xp.evaluate("count(//jersey:extended)", d, XPathConstants.STRING);
        assertEquals("31", val);
    }

    /**
     * Tests limited wadl with only user resources.
     *
     * @throws ParserConfigurationException
     * @throws XPathExpressionException
     * @throws IOException
     * @throws SAXException
     */
    @Test
    public void testLimitedWadl() throws ParserConfigurationException, XPathExpressionException, IOException,
            SAXException {
        Response response = target("/application.wadl").request(MediaTypes.WADL_TYPE).get();
        assertEquals(200, response.getStatus());
        File tmpFile = response.readEntity(File.class);
        DocumentBuilderFactory bf = DocumentBuilderFactory.newInstance();
        bf.setNamespaceAware(true);
        bf.setValidating(false);
        if (!SaxHelper.isXdkDocumentBuilderFactory(bf)) {
            bf.setXIncludeAware(false);
        }
        DocumentBuilder b = bf.newDocumentBuilder();
        Document d = b.parse(tmpFile);
        printSource(new DOMSource(d));
        XPath xp = XPathFactory.newInstance().newXPath();
        xp.setNamespaceContext(new SimpleNamespaceResolver("wadl", "http://wadl.dev.java.net/2009/02"));

        // check base URI
        String val = (String) xp.evaluate("/wadl:application/wadl:resources/@base", d, XPathConstants.STRING);
        assertEquals(val, getBaseUri().toString());
        // check total number of resources is 8
        val = (String) xp.evaluate("count(//wadl:resource)", d, XPathConstants.STRING);
        assertEquals("2", val);

        val = (String) xp.evaluate("count(//wadl:resource[@path='all-extended'])", d, XPathConstants.STRING);
        assertEquals("0", val);

        val = (String) xp.evaluate("count(//wadl:resource[@path='resource'])", d, XPathConstants.STRING);
        assertEquals("1", val);

        val = (String) xp.evaluate("count(//wadl:resource[@path='application.wadl'])", d, XPathConstants.STRING);
        assertEquals("0", val);

        xp.setNamespaceContext(new SimpleNamespaceResolver("jersey", "http://jersey.java.net/"));
        val = (String) xp.evaluate("count(//jersey:extended)", d, XPathConstants.STRING);
        assertEquals("0", val);
    }

    public static void printSource(Source source) {
        try {
            System.out.println("---------------------");
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            Properties oprops = new Properties();
            oprops.put(OutputKeys.OMIT_XML_DECLARATION, "yes");
            oprops.put(OutputKeys.INDENT, "yes");
            oprops.put(OutputKeys.METHOD, "xml");
            trans.setOutputProperties(oprops);
            trans.transform(source, new StreamResult(System.out));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
