/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-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.linking;

import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Filter;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.MatchResult;
import java.util.zip.ZipEntry;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.UriBuilder;

import javax.xml.bind.annotation.XmlTransient;

import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap;
import org.glassfish.jersey.linking.mapping.ResourceMappingContext;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.RuntimeResource;
import org.glassfish.jersey.uri.UriTemplate;

import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

/**
 * @author Mark Hadley
 * @author Gerard Davison (gerard.davison at oracle.com)
 */
public class FieldProcessorTest {

    private static final Logger LOG = Logger.getLogger(FieldProcessor.class.getName());

    ExtendedUriInfo mockUriInfo = new ExtendedUriInfo() {

        private static final String baseURI = "http://example.com/application/resources";

        private MultivaluedMap queryParams = new MultivaluedStringMap();

        @Override
        public String getPath() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public String getPath(boolean decode) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<PathSegment> getPathSegments() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<PathSegment> getPathSegments(boolean decode) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public URI getRequestUri() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public UriBuilder getRequestUriBuilder() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public URI getAbsolutePath() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public UriBuilder getAbsolutePathBuilder() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public URI getBaseUri() {
            return getBaseUriBuilder().build();
        }

        @Override
        public UriBuilder getBaseUriBuilder() {
            return UriBuilder.fromUri(baseURI);
        }

        @Override
        public MultivaluedMap<String, String> getPathParameters() {
            return new MultivaluedStringMap();
        }

        @Override
        public MultivaluedMap<String, String> getPathParameters(boolean decode) {
            return new MultivaluedStringMap();
        }

        @Override
        public MultivaluedMap<String, String> getQueryParameters() {
            return queryParams;
        }

        @Override
        public MultivaluedMap<String, String> getQueryParameters(boolean decode) {
            return queryParams;
        }

        @Override
        public List<String> getMatchedURIs() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<String> getMatchedURIs(boolean decode) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<Object> getMatchedResources() {
            Object dummyResource = new Object() {
            };
            return Collections.singletonList(dummyResource);
        }

        @Override
        public Throwable getMappedThrowable() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<MatchResult> getMatchedResults() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<UriTemplate> getMatchedTemplates() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<PathSegment> getPathSegments(String name) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<PathSegment> getPathSegments(String name, boolean decode) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<RuntimeResource> getMatchedRuntimeResources() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public ResourceMethod getMatchedResourceMethod() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public Resource getMatchedModelResource() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<ResourceMethod> getMatchedResourceLocators() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public List<Resource> getLocatorSubResources() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public URI resolve(URI uri) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public URI relativize(URI uri) {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    };

    private final ResourceMappingContext mockRmc = new ResourceMappingContext() {

        @Override
        public ResourceMappingContext.Mapping getMapping(Class<?> resource) {
            return null;
        }
    };

    private static final String TEMPLATE_A = "foo";

    public static class TestClassD {

        @InjectLink(value = TEMPLATE_A, style = InjectLink.Style.RELATIVE_PATH)
        private String res1;

        @InjectLink(value = TEMPLATE_A, style = InjectLink.Style.RELATIVE_PATH)
        private URI res2;
    }

    @Test
    public void testProcessLinks() {
        LOG.info("Links");

        FieldProcessor<TestClassD> instance = new FieldProcessor(TestClassD.class);
        TestClassD testClass = new TestClassD();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals(TEMPLATE_A, testClass.res1);
        assertEquals(TEMPLATE_A, testClass.res2.toString());
    }

    private static final String TEMPLATE_B = "widgets/{id}";

    public static class TestClassE {

        @InjectLink(value = TEMPLATE_B, style = InjectLink.Style.RELATIVE_PATH)
        private String link;

        private String id;

        public TestClassE(String id) {
            this.id = id;
        }

        public String getId() {
            return id;
        }
    }

    @Test
    public void testProcessLinksWithFields() {
        LOG.info("Links from field values");
        FieldProcessor<TestClassE> instance = new FieldProcessor(TestClassE.class);
        TestClassE testClass = new TestClassE("10");
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("widgets/10", testClass.link);
    }

    public static class TestClassF {

        @InjectLink(value = TEMPLATE_B, style = InjectLink.Style.RELATIVE_PATH)
        private String thelink;

        private String id;
        private TestClassE nested;

        public TestClassF(String id, TestClassE e) {
            this.id = id;
            this.nested = e;
        }

        public String getId() {
            return id;
        }
    }

    @Test
    public void testNesting() {
        LOG.info("Nesting");
        FieldProcessor<TestClassF> instance = new FieldProcessor(TestClassF.class);
        TestClassE nested = new TestClassE("10");
        TestClassF testClass = new TestClassF("20", nested);
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("widgets/20", testClass.thelink);
        assertEquals("widgets/10", testClass.nested.link);
    }

    @Test
    public void testArray() {
        LOG.info("Array");
        FieldProcessor<TestClassE[]> instance = new FieldProcessor(TestClassE[].class);
        TestClassE item1 = new TestClassE("10");
        TestClassE item2 = new TestClassE("20");
        TestClassE array[] = {item1, item2};
        instance.processLinks(array, mockUriInfo, mockRmc);
        assertEquals("widgets/10", array[0].link);
        assertEquals("widgets/20", array[1].link);
    }

    @Test
    public void testCollection() {
        LOG.info("Collection");
        FieldProcessor<List> instance = new FieldProcessor(List.class);
        TestClassE item1 = new TestClassE("10");
        TestClassE item2 = new TestClassE("20");
        List<TestClassE> list = Arrays.asList(item1, item2);
        instance.processLinks(list, mockUriInfo, mockRmc);
        assertEquals("widgets/10", list.get(0).link);
        assertEquals("widgets/20", list.get(1).link);
    }

    public static class TestClassG {

        @InjectLink(value = TEMPLATE_B, style = InjectLink.Style.RELATIVE_PATH)
        private String relativePath;

        @InjectLink(value = TEMPLATE_B, style = InjectLink.Style.ABSOLUTE_PATH)
        private String absolutePath;

        @InjectLink(value = TEMPLATE_B, style = InjectLink.Style.ABSOLUTE)
        private String absolute;

        @InjectLink(TEMPLATE_B)
        private String defaultStyle;

        private String id;

        public TestClassG(String id) {
            this.id = id;
        }

        public String getId() {
            return id;
        }
    }

    @Test
    public void testLinkStyles() {
        LOG.info("Link styles");
        FieldProcessor<TestClassG> instance = new FieldProcessor(TestClassG.class);
        TestClassG testClass = new TestClassG("10");
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("widgets/10", testClass.relativePath);
        assertEquals("/application/resources/widgets/10", testClass.absolutePath);
        assertEquals("/application/resources/widgets/10", testClass.defaultStyle);
        assertEquals("http://example.com/application/resources/widgets/10", testClass.absolute);
    }

    public static class TestClassH {

        @InjectLink(TEMPLATE_B)
        private String link;

        public String getId() {
            return "10";
        }
    }

    @Test
    public void testComputedProperty() {
        LOG.info("Computed property");
        FieldProcessor<TestClassH> instance = new FieldProcessor(TestClassH.class);
        TestClassH testClass = new TestClassH();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/widgets/10", testClass.link);
    }

    public static class TestClassI {

        @InjectLink("widgets/${entity.id}")
        private String link;

        public String getId() {
            return "10";
        }
    }

    @Test
    public void testEL() {
        LOG.info("El link");
        FieldProcessor<TestClassI> instance = new FieldProcessor(TestClassI.class);
        TestClassI testClass = new TestClassI();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/widgets/10", testClass.link);
    }

    public static class TestClassJ {

        @InjectLink("widgets/${entity.id}/widget/{id}")
        private String link;

        public String getId() {
            return "10";
        }
    }

    @Test
    public void testMixed() {
        LOG.info("Mixed EL and template vars link");
        FieldProcessor<TestClassJ> instance = new FieldProcessor(TestClassJ.class);
        TestClassJ testClass = new TestClassJ();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/widgets/10/widget/10", testClass.link);
    }

    public static class DependentInnerBean {

        @InjectLink("${entity.id}")
        public String outerUri;
        @InjectLink("${instance.id}")
        public String innerUri;

        public String getId() {
            return "inner";
        }
    }

    public static class OuterBean {

        public DependentInnerBean inner = new DependentInnerBean();

        public String getId() {
            return "outer";
        }
    }

    @Test
    public void testELScopes() {
        LOG.info("EL scopes");
        FieldProcessor<OuterBean> instance = new FieldProcessor(OuterBean.class);
        OuterBean testClass = new OuterBean();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/inner", testClass.inner.innerUri);
        assertEquals("/application/resources/outer", testClass.inner.outerUri);
    }

    public static class BoundLinkBean {

        @InjectLink(value = "{id}", bindings = {@Binding(name = "id", value = "${instance.name}")})
        public String uri;

        public String getName() {
            return "name";
        }
    }

    @Test
    public void testELBinding() {
        LOG.info("EL binding");
        FieldProcessor<BoundLinkBean> instance = new FieldProcessor(BoundLinkBean.class);
        BoundLinkBean testClass = new BoundLinkBean();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/name", testClass.uri);
    }

    public static class BoundLinkOnLinkBean {

        @InjectLink(value = "{id}",
                bindings = {@Binding(name = "id", value = "${instance.name}")},
                rel = "self")
        public Link link;

        public String getName() {
            return "name";
        }
    }

    @Test
    public void testELBindingOnLink() {
        LOG.info("EL binding");
        FieldProcessor<BoundLinkOnLinkBean> instance = new FieldProcessor(BoundLinkOnLinkBean.class);
        BoundLinkOnLinkBean testClass = new BoundLinkOnLinkBean();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/name", testClass.link.getUri().toString());
        assertEquals("self", testClass.link.getRel());
    }

    public static class BoundLinkOnLinksBean {

        @InjectLinks({
                @InjectLink(value = "{id}",
                        bindings = {@Binding(name = "id", value = "${instance.name}")},
                        rel = "self"),
                @InjectLink(value = "{id}",
                        bindings = {@Binding(name = "id", value = "${instance.name}")},
                        rel = "other"),

        })
        public List<Link> links;

        @InjectLinks({
                @InjectLink(value = "{id}",
                        bindings = {@Binding(name = "id", value = "${instance.name}")},
                        rel = "self"),
                @InjectLink(value = "{id}",
                        bindings = {@Binding(name = "id", value = "${instance.name}")},
                        rel = "other"),

        })
        public Link[] linksArray;

        public String getName() {
            return "name";
        }
    }

    @Test
    public void testELBindingOnLinks() {
        LOG.info("EL binding");
        FieldProcessor<BoundLinkOnLinksBean> instance = new FieldProcessor(BoundLinkOnLinksBean.class);
        BoundLinkOnLinksBean testClass = new BoundLinkOnLinksBean();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/name", testClass.links.get(0).getUri().toString());
        assertEquals("self", testClass.links.get(0).getRel());
        assertEquals("other", testClass.links.get(1).getRel());

        assertEquals("/application/resources/name", testClass.linksArray[0].getUri().toString());
        assertEquals("self", testClass.linksArray[0].getRel());
        assertEquals("other", testClass.linksArray[1].getRel());

    }

    public static class ConditionalLinkBean {

        @InjectLink(value = "{id}", condition = "${entity.uri1Enabled}")
        public String uri1;

        @InjectLink(value = "{id}", condition = "${entity.uri2Enabled}")
        public String uri2;

        public String getId() {
            return "name";
        }

        public boolean isUri1Enabled() {
            return true;
        }

        public boolean isUri2Enabled() {
            return false;
        }
    }

    @Test
    public void testCondition() {
        LOG.info("Condition");
        FieldProcessor<ConditionalLinkBean> instance = new FieldProcessor(ConditionalLinkBean.class);
        ConditionalLinkBean testClass = new ConditionalLinkBean();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/name", testClass.uri1);
        assertEquals(null, testClass.uri2);
    }

    @Path("a")
    public static class SubResource {

        @Path("b")
        @GET
        public String getB() {
            return "hello world";
        }
    }

    public static class SubResourceBean {

        @InjectLink(resource = SubResource.class, method = "getB")
        public String uri;
    }

    @Test
    public void testSubresource() {
        LOG.info("Subresource");
        FieldProcessor<SubResourceBean> instance = new FieldProcessor(SubResourceBean.class);
        SubResourceBean testClass = new SubResourceBean();
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/a/b", testClass.uri);
    }

    @Path("a")
    public static class QueryResource {

        @Path("b")
        @GET
        public String getB(@QueryParam("query") String query, @QueryParam("query2") String query2) {
            return "hello world";
        }
    }

    public static class QueryResourceBean {

        public String getQueryParam() {
            return queryExample;
        }

        private String queryExample;

        public QueryResourceBean(String queryExample, String queryExample2) {
            this.queryExample = queryExample;
            this.queryExample2 = queryExample2;
        }

        public String getQueryParam2() {
            return queryExample2;
        }

        private String queryExample2;

        @InjectLink(resource = QueryResource.class, method = "getB",
                bindings = {
                        @Binding(name = "query", value = "${instance.queryParam}"),
                        @Binding(name = "query2", value = "${instance.queryParam2}")
                })
        public String uri;
    }

    public static class QueryResourceBeanNoBindings {
        //query parameters will be populated from uriInfo
        //JERSEY-2863
        @InjectLink(resource = QueryResource.class, method = "getB")
        public String uri;
    }

    @Test
    public void testQueryResource() {
        LOG.info("QueryResource");
        FieldProcessor<QueryResourceBean> instance = new FieldProcessor(QueryResourceBean.class);
        QueryResourceBean testClass = new QueryResourceBean("queryExample", null);
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/a/b?query=queryExample&query2=", testClass.uri);
    }

    @Test
    public void testDoubleQueryResource() {
        LOG.info("QueryResource");
        FieldProcessor<QueryResourceBean> instance = new FieldProcessor(QueryResourceBean.class);
        QueryResourceBean testClass = new QueryResourceBean("queryExample", "queryExample2");
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/a/b?query=queryExample&query2=queryExample2", testClass.uri);
    }

    @Test
    public void testQueryResourceWithoutBindings() {
        LOG.info("QueryResource");
        FieldProcessor<QueryResourceBeanNoBindings> instance = new FieldProcessor(QueryResourceBeanNoBindings.class);
        QueryResourceBeanNoBindings testClass = new QueryResourceBeanNoBindings();
        mockUriInfo.getQueryParameters().putSingle("query", "queryExample");
        mockUriInfo.getQueryParameters().putSingle("query2", "queryExample2");
        assertEquals("queryExample", mockUriInfo.getQueryParameters().getFirst("query"));
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("/application/resources/a/b?query=queryExample&query2=queryExample2", testClass.uri);
        //clean mock
        mockUriInfo.getQueryParameters().clear();
    }

    public static class TestClassK {

        public static final ZipEntry zipEntry = new ZipEntry("entry");
    }

    public static class TestClassL {

        public final ZipEntry zipEntry = new ZipEntry("entry");
    }

    private class LoggingFilter implements Filter {

        private int count = 0;

        @Override
        public synchronized boolean isLoggable(LogRecord logRecord) {
            if (logRecord.getThrown() instanceof IllegalAccessException) {
                count++;
                return false;
            }
            return true;
        }

        public int getCount() {
            return count;
        }
    }

    @Test
    public void testKL() {
        final LoggingFilter lf = new LoggingFilter();

        Logger.getLogger(FieldDescriptor.class.getName()).setFilter(lf);
        assertTrue(lf.getCount() == 0);

        FieldProcessor<TestClassK> instanceK = new FieldProcessor(TestClassK.class);
        TestClassK testClassK = new TestClassK();
        instanceK.processLinks(testClassK, mockUriInfo, mockRmc);

        assertTrue(lf.getCount() == 0);

        FieldProcessor<TestClassL> instanceL = new FieldProcessor(TestClassL.class);
        TestClassL testClassL = new TestClassL();
        instanceL.processLinks(testClassL, mockUriInfo, mockRmc);

        assertTrue(lf.getCount() == 0);

        Logger.getLogger(FieldDescriptor.class.getName()).setFilter(null);

    }

    public static class TestClassM {

        @InjectLink(value = TEMPLATE_B, style = InjectLink.Style.RELATIVE_PATH)
        private String thelink;

        private String id;

        @InjectLinkNoFollow
        private TestClassE nested;

        @XmlTransient
        private TestClassE transientNested;

        public TestClassM(String id, TestClassE e, TestClassE transientNested) {
            this.id = id;
            this.nested = e;
            this.transientNested = transientNested;
        }

        public String getId() {
            return id;
        }
    }

    @Test
    public void testNoRecursiveNesting() {
        LOG.info("No Recursive Nesting");
        FieldProcessor<TestClassM> instance = new FieldProcessor(TestClassM.class);
        TestClassE nested = new TestClassE("10");
        TestClassE transientNested = new TestClassE("30");
        TestClassM testClass = new TestClassM("20", nested, transientNested);
        instance.processLinks(testClass, mockUriInfo, mockRmc);
        assertEquals("widgets/20", testClass.thelink);
        assertEquals(null, testClass.nested.link);
        assertEquals(null, testClass.transientNested.link);
    }

    public static class TestClassN {
        // Simulate object injected by JPA
        // in order to test a fix for JERSEY-2625
        private transient Iterable res1 = new Iterable() {
            @Override
            public Iterator iterator() {
                throw new RuntimeException("Declarative linking feature is incorrectly processing a transient iterator");
            }

        };
    }

    @Test
    public void testIgnoreTransient() {
        TestClassN testClass = new TestClassN();
        FieldProcessor<TestClassN> instance = new FieldProcessor(TestClassN.class);
        instance.processLinks(testClass, mockUriInfo, mockRmc);
    }

}
