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

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.ws.rs.Path;

import org.glassfish.jersey.Severity;
import org.glassfish.jersey.internal.Errors;
import org.glassfish.jersey.internal.guava.Preconditions;
import org.glassfish.jersey.internal.util.collection.Value;
import org.glassfish.jersey.internal.util.collection.Values;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.glassfish.jersey.server.model.internal.ModelHelper;
import org.glassfish.jersey.uri.PathPattern;

/**
 * Model of a single resource component.
 * <p>
 * Resource component model represents a collection of {@link ResourceMethod methods}
 * grouped under the same parent request path template. {@code Resource} class is also
 * the main entry point to the programmatic resource modeling API that provides ability
 * to programmatically extend the existing JAX-RS annotated resource classes or build
 * new resource models that may be utilized by Jersey runtime.
 * </p>
 * <p>
 * For example:
 * <pre>
 * &#64;Path("hello")
 * public class HelloResource {
 *      &#64;GET
 *      &#64;Produces("text/plain")
 *      public String sayHello() {
 *          return "Hello!";
 *      }
 * }
 *
 * ...
 *
 * // Register the annotated resource.
 * ResourceConfig resourceConfig = new ResourceConfig(HelloResource.class);
 *
 * // Add new "hello2" resource using the annotated resource class
 * // and overriding the resource path.
 * Resource.Builder resourceBuilder =
 *         Resource.builder(HelloResource.class, new LinkedList&lt;ResourceModelIssue&gt;())
 *         .path("hello2");
 *
 * // Add a new (virtual) sub-resource method to the "hello2" resource.
 * resourceBuilder.addChildResource("world").addMethod("GET")
 *         .produces("text/plain")
 *         .handledBy(new Inflector&lt;Request, String&gt;() {
 *                 &#64;Override
 *                 public String apply(Request request) {
 *                     return "Hello World!";
 *                 }
 *         });
 *
 * // Register the new programmatic resource in the application's configuration.
 * resourceConfig.registerResources(resourceBuilder.build());
 * </pre>
 * The following table illustrates the supported requests and provided responses
 * for the application configured in the example above.
 * <table>
 * <tr>
 * <th>Request</th><th>Response</th><th>Method invoked</th>
 * </tr>
 * <tr>
 * <td>{@code "GET /hello"}</td><td>{@code "Hello!"}</td><td>{@code HelloResource.sayHello()}</td>
 * </tr>
 * <tr>
 * <td>{@code "GET /hello2"}</td><td>{@code "Hello!"}</td><td>{@code HelloResource.sayHello()}</td>
 * </tr>
 * <tr>
 * <td>{@code "GET /hello2/world"}</td><td>{@code "Hello World!"}</td><td>{@code Inflector.apply()}</td>
 * </tr>
 * </table>
 * </p>
 *
 * @author Marek Potociar (marek.potociar at oracle.com)
 * @author Miroslav Fuksa
 */
public final class Resource implements Routed, ResourceModelComponent {

    /**
     * Immutable resource data holder.
     */
    private static class Data {

        private final List<String> names;
        private final String path;
        private final PathPattern pathPattern;

        private final List<ResourceMethod.Data> resourceMethods;
        private final ResourceMethod.Data subResourceLocator;
        private final List<Resource.Data> childResources;

        private final Set<Class<?>> handlerClasses;
        private final Set<Object> handlerInstances;

        private final boolean extended;

        /**
         * Create a new immutable resource data holder from the supplied parameters.
         *
         * @param names            resource names.
         * @param path             resource path.
         * @param resourceMethods  child resource methods.
         * @param subResourceLocator          child resource locator.
         * @param childResources   child sub-resources.
         * @param handlerClasses   handler classes handling the resource methods.
         * @param handlerInstances handler instances handling the resource methods.
         * @param extended         flag indicating whether the resource is extended
         */
        private Data(
                final List<String> names,
                final String path,
                final List<ResourceMethod.Data> resourceMethods,
                final ResourceMethod.Data subResourceLocator,
                final List<Data> childResources,
                final Set<Class<?>> handlerClasses,
                final Set<Object> handlerInstances,
                boolean extended) {

            this.extended = extended;

            this.names = Resource.immutableCopy(names);
            this.path = path;
            this.pathPattern = (path == null || path.isEmpty())
                    ? PathPattern.OPEN_ROOT_PATH_PATTERN
                    : new PathPattern(path, PathPattern.RightHandPath.capturingZeroOrMoreSegments);
            this.resourceMethods = Resource.immutableCopy(resourceMethods);
            this.subResourceLocator = subResourceLocator;
            this.childResources = Collections.unmodifiableList(childResources); // no need to deep-copy the list

            this.handlerClasses = Resource.immutableCopy(handlerClasses);
            this.handlerInstances = Resource.immutableCopy(handlerInstances);
        }

        @Override
        public String toString() {
            return "Resource{"
                    + ((path == null) ? "[unbound], " : "\"" + path + "\", ")
                    + childResources.size() + " child resources, "
                    + resourceMethods.size() + " resource methods, "
                    + (subResourceLocator == null ? "0" : "1") + " sub-resource locator, "
                    + handlerClasses.size() + " method handler classes, "
                    + handlerInstances.size() + " method handler instances"
                    + '}';
        }
    }

    /**
     * Resource model component builder.
     */
    public static final class Builder {

        private List<String> names;
        private String path;

        private final Set<ResourceMethod.Builder> methodBuilders;
        private final Set<Resource.Builder> childResourceBuilders;
        private final List<Resource.Data> childResources;

        private final List<ResourceMethod.Data> resourceMethods;
        private ResourceMethod.Data subResourceLocator;

        private final Set<Class<?>> handlerClasses;
        private final Set<Object> handlerInstances;

        private final Resource.Builder parentResource;

        private boolean extended;

        private Builder(final Resource.Builder parentResource) {
            this.methodBuilders = new LinkedHashSet<>();
            this.childResourceBuilders = new LinkedHashSet<>();
            this.childResources = new LinkedList<>();
            this.resourceMethods = new LinkedList<>();
            this.handlerClasses = Collections.newSetFromMap(new IdentityHashMap<>());
            this.handlerInstances = Collections.newSetFromMap(new IdentityHashMap<>());
            this.parentResource = parentResource;

            name("[unnamed]");
        }

        private Builder(final String path) {
            this((Resource.Builder) null);
            path(path);
        }

        private Builder(final String path, final Resource.Builder parentResource) {
            this(parentResource);
            this.path = path;
        }

        private Builder() {
            this((Resource.Builder) null);
        }

        private boolean isEmpty() {
            return this.path == null
                    && methodBuilders.isEmpty()
                    && childResourceBuilders.isEmpty()
                    && resourceMethods.isEmpty()
                    && childResources.isEmpty()
                    && subResourceLocator == null;
        }

        /**
         * Define a new name of the built resource.
         * <p/>
         * The resource model name is typically used for reporting
         * purposes (e.g. validation etc.).
         *
         * @param name new name of the resource.
         * @return updated builder object.
         * @see org.glassfish.jersey.server.model.Resource#getName()
         */
        public Builder name(final String name) {
            this.names = new ArrayList<>();
            this.names.add(name);
            return this;
        }

        /**
         * Define a new path for the built resource.
         * <p/>
         * NOTE: Invoking this method marks a resource as a root resource.
         *
         * @param path new path for the resource.
         * @return updated builder object.
         */
        public Builder path(final String path) {
            this.path = path;
            return this;
        }

        /**
         * Add a new method model to the resource for processing requests of
         * the specified HTTP method.
         * <p/>
         * The returned builder is automatically bound to the the resource. It is
         * not necessary to invoke the {@link ResourceMethod.Builder#build() build()}
         * method on the method builder after setting all the data. This will be
         * done automatically when the resource is built.
         *
         * @param httpMethod HTTP method that will be processed by the method.
         * @return a new resource method builder.
         */
        public ResourceMethod.Builder addMethod(final String httpMethod) {
            ResourceMethod.Builder builder = new ResourceMethod.Builder(this);
            methodBuilders.add(builder);
            return builder.httpMethod(httpMethod);
        }

        /**
         * Add a new arbitrary method model to the resource.
         * <p/>
         * The returned builder is automatically bound to the the resource. It is
         * not necessary to invoke the {@link ResourceMethod.Builder#build() build()}
         * method on the method builder after setting all the data. This will be
         * done automatically when the resource is built.
         *
         * @return a new resource method builder.
         */
        public ResourceMethod.Builder addMethod() {
            ResourceMethod.Builder builder = new ResourceMethod.Builder(this);
            methodBuilders.add(builder);
            return builder;
        }

        /**
         * Add a new method model that is a copy of the given {@code resourceMethod}.
         * <p/>
         * The returned builder is automatically bound to the the resource. It is
         * not necessary to invoke the {@link ResourceMethod.Builder#build() build()}
         * method on the method builder after setting all the data. This will be
         * done automatically when the resource is built.
         *
         * @param resourceMethod The resource method based on which the new method builder
         *                       should be created.
         * @return a new resource method builder.
         */
        public ResourceMethod.Builder addMethod(ResourceMethod resourceMethod) {
            ResourceMethod.Builder builder = new ResourceMethod.Builder(this, resourceMethod);
            methodBuilders.add(builder);
            return builder;
        }

        /**
         * Get a method builder for an existing resource method.
         * <p/>
         * The original method is removed from the resource and a builder based on it is added.
         *
         * @param resourceMethod The resource method to be replaced by a builder.
         * @return a new resource method builder.
         * @throws java.lang.IllegalArgumentException in case the method to be updated is not registered
         *                                            in this resource builder.
         * @since 2.18
         */
        public ResourceMethod.Builder updateMethod(ResourceMethod resourceMethod) {
            final boolean removed = resourceMethods.remove(resourceMethod.getData());
            if (!removed) {
                throw new IllegalArgumentException(
                        LocalizationMessages.RESOURCE_UPDATED_METHOD_DOES_NOT_EXIST(resourceMethod.toString()));
            }
            ResourceMethod.Builder builder = new ResourceMethod.Builder(this, resourceMethod);
            methodBuilders.add(builder);
            return builder;
        }

        /**
         * Add a new child resource to the resource.
         * <p/>
         * The returned builder is automatically bound to the the resource. It is
         * not necessary to invoke the {@link Resource.Builder#build() build()}
         * method on the resource builder after setting all the data. This will be
         * done automatically when the resource is built.
         *
         * @param relativePath The path of the new child resource relative to this resource.
         * @return child resource builder.
         * @throws java.lang.IllegalStateException in case the child resource for this path exists already.
         */
        public Builder addChildResource(String relativePath) {
            if (this.parentResource != null) {
                throw new IllegalStateException(LocalizationMessages.RESOURCE_ADD_CHILD_ALREADY_CHILD());
            }
            final Builder resourceBuilder = new Builder(relativePath, this);

            childResourceBuilders.add(resourceBuilder);
            return resourceBuilder;
        }

        /**
         * Add an existing Resource as a child resource of current resource.
         *
         * @param resource Resource to be added as child resource.
         */
        public void addChildResource(Resource resource) {
            this.childResources.add(resource.data);
        }

        /**
         * Replace an existing child resource with a newly supplied one.
         *
         * @param replacedResource old resource to be replaced.
         * @param newResource      new resource to add.
         * @throws java.lang.IllegalArgumentException in case there is no such child resource to be replaced.
         * @since 2.18
         */
        public void replaceChildResource(Resource replacedResource, Resource newResource) {
            final boolean removed = this.childResources.remove(replacedResource.data);
            if (!removed) {
                throw new IllegalArgumentException(
                        LocalizationMessages.RESOURCE_REPLACED_CHILD_DOES_NOT_EXIST(replacedResource.toString()));
            }
            addChildResource(newResource);
        }

        /**
         * Merge methods from a given resource model into this resource model builder.
         *
         * @param resource to be merged into this resource model builder.
         * @return updated builder object.
         */
        public Builder mergeWith(final Resource resource) {
            mergeWith(resource.data);
            return this;
        }

        /**
         * Set the flag indicating whether the resource is extended or is a core of exposed RESTful API.
         * <p/>
         * The method defines the flag available at {@link org.glassfish.jersey.server.model.Resource#isExtended()}.
         * <p>
         * Extended resource model components are helper components that are not considered as a core of a
         * RESTful API. These can be for example {@code OPTIONS} {@link ResourceMethod resource methods}
         * added by {@link org.glassfish.jersey.server.model.ModelProcessor model processors}
         * or {@code application.wadl} resource producing the WADL. Both resource are rather supportive
         * than the core of RESTful API.
         * </p>
         * <p>
         * If not set the resource will not be defined as extended by default.
         * </p>
         *
         * @param extended If {@code true} then resource is marked as extended.
         * @return updated builder object.
         * @see org.glassfish.jersey.server.model.ExtendedResource
         * @since 2.5.1
         */
        public Builder extended(boolean extended) {
            this.extended = extended;
            return this;
        }

        /**
         * Get the flag indicating whether the resource method is extended or is a core of exposed RESTful API.
         *
         * @return {@code true} if the method is extended.
         */
        /* package */ boolean isExtended() {
            return extended;
        }

        private Builder mergeWith(final Resource.Data resourceData) {
            this.resourceMethods.addAll(resourceData.resourceMethods);
            this.childResources.addAll(resourceData.childResources);

            if (subResourceLocator != null && resourceData.subResourceLocator != null) {
                Errors.processWithException(new Runnable() {
                    @Override
                    public void run() {
                        Errors.error(
                                this,
                                LocalizationMessages.RESOURCE_MERGE_CONFLICT_LOCATORS(Builder.this, resourceData, path),
                                Severity.FATAL);
                    }
                });
            } else if (resourceData.subResourceLocator != null) {
                this.subResourceLocator = resourceData.subResourceLocator;
            }

            this.handlerClasses.addAll(resourceData.handlerClasses);
            this.handlerInstances.addAll(resourceData.handlerInstances);

            this.names.addAll(resourceData.names);

            return this;
        }

        /**
         * Merge methods from a given resource model builder into this resource model
         * builder.
         * <p>
         * NOTE: Any "open" method builders in the supplied {@code resourceBuilder} that have
         * not been {@link org.glassfish.jersey.server.model.ResourceMethod.Builder#build()
         * explicitly converted to method models} will be closed as part of this merge operation
         * before merging the resource builder instances.
         * </p>
         *
         * @param resourceBuilder to be merged into this resource model builder.
         * @return updated builder object.
         */
        public Builder mergeWith(final Builder resourceBuilder) {
            resourceBuilder.processMethodBuilders();

            this.resourceMethods.addAll(resourceBuilder.resourceMethods);
            this.childResources.addAll(resourceBuilder.childResources);
            if (Resource.Builder.this.subResourceLocator != null && resourceBuilder.subResourceLocator != null) {
                Errors.processWithException(new Runnable() {
                    @Override
                    public void run() {
                        Errors.warning(this, LocalizationMessages.RESOURCE_MERGE_CONFLICT_LOCATORS(Resource.Builder.this,
                                resourceBuilder, path));
                    }
                });
            } else if (resourceBuilder.subResourceLocator != null) {
                this.subResourceLocator = resourceBuilder.subResourceLocator;
            }
            this.handlerClasses.addAll(resourceBuilder.handlerClasses);
            this.handlerInstances.addAll(resourceBuilder.handlerInstances);
            this.names.addAll(resourceBuilder.names);
            return this;
        }

        /**
         * Called when a new resource, sub-resource and sub-resource locator method
         * was built and should be registered with the resource builder.
         * <p>
         * This is a friend call-back API exposed for a use by a {@link ResourceMethod.Builder
         * ResourceMethod.Builder}.
         * </p>
         *
         * @param builder    builder instance that built the method.
         * @param methodData new resource, sub-resource or sub-resource locator
         */
        void onBuildMethod(ResourceMethod.Builder builder, ResourceMethod.Data methodData) {
            Preconditions.checkState(methodBuilders.remove(builder),
                    "Resource.Builder.onBuildMethod() invoked from a resource method builder "
                            + "that is not registered in the resource builder instance.");

            switch (methodData.getType()) {
                case RESOURCE_METHOD:
                    resourceMethods.add(methodData);
                    break;
                case SUB_RESOURCE_LOCATOR:
                    if (subResourceLocator != null) {
                        Errors.processWithException(new Runnable() {
                            @Override
                            public void run() {
                                Errors.error(
                                        this,
                                        LocalizationMessages.AMBIGUOUS_SRLS(this, path),
                                        Severity.FATAL);
                            }
                        });

                    }
                    subResourceLocator = methodData;
                    break;
            }

            final MethodHandler methodHandler = methodData.getInvocable().getHandler();
            if (methodHandler.isClassBased()) {
                handlerClasses.add(methodHandler.getHandlerClass());
            } else {
                handlerInstances.add(methodHandler.getHandlerInstance());
            }
        }

        private void onBuildChildResource(Builder childResourceBuilder, Resource.Data childResourceData) {
            Preconditions.checkState(childResourceBuilders.remove(childResourceBuilder),
                    "Resource.Builder.onBuildChildResource() invoked from a resource builder "
                            + "that is not registered in the resource builder instance as a child resource builder.");
            childResources.add(childResourceData);

        }

        private List<Resource.Data> mergeResources(List<Resource.Data> resources) {
            List<Resource.Data> mergedResources = new ArrayList<>();
            for (int i = 0; i < resources.size(); i++) {
                Resource.Data outer = resources.get(i);
                Resource.Builder builder = null;

                for (int j = i + 1; j < resources.size(); j++) {
                    Resource.Data inner = resources.get(j);

                    if (outer.path.equals(inner.path)) {
                        if (builder == null) {
                            builder = Resource.builder(outer);
                        }
                        builder.mergeWith(inner);
                        resources.remove(j);
                        //noinspection AssignmentToForLoopParameter
                        j--;
                    }
                }
                if (builder == null) {
                    mergedResources.add(outer);
                } else {
                    mergedResources.add(builder.buildResourceData());
                }
            }
            return mergedResources;
        }

        private Data buildResourceData() {
            if (parentResource != null && parentResource.isExtended()) {
                this.extended = true;
            }

            processMethodBuilders();
            processChildResourceBuilders();

            final List<Data> mergedChildResources = mergeResources(childResources);
            Set<Class<?>> classes = new HashSet<>(handlerClasses);
            Set<Object> instances = new HashSet<>(handlerInstances);
            for (Data childResource : mergedChildResources) {
                classes.addAll(childResource.handlerClasses);
                instances.addAll(childResource.handlerInstances);
            }

            if (areAllMembersExtended(mergedChildResources)) {
                extended = true;
            }

            final Data resourceData = new Data(
                    names,
                    path,
                    resourceMethods,
                    subResourceLocator,
                    mergedChildResources,
                    classes,
                    instances,
                    extended);

            if (parentResource != null) {
                parentResource.onBuildChildResource(this, resourceData);
            }
            return resourceData;
        }

        private boolean areAllMembersExtended(List<Data> mergedChildResources) {
            boolean allExtended = true;
            for (ResourceMethod.Data resourceMethod : resourceMethods) {
                if (!resourceMethod.isExtended()) {
                    allExtended = false;
                }
            }
            if (subResourceLocator != null && !subResourceLocator.isExtended()) {
                allExtended = false;
            }

            for (Data childResource : mergedChildResources) {
                if (!childResource.extended) {
                    allExtended = false;
                }
            }
            return allExtended;
        }

        /**
         * Build a new resource model.
         *
         * @return new (immutable) resource model.
         */
        public Resource build() {
            final Data resourceData = buildResourceData();
            return new Resource(null, resourceData);
        }

        private void processMethodBuilders() {
            // We have to iterate the set this way to prevent ConcurrentModificationExceptions
            // caused by the nested invocation of Set.remove(...) in Resource.Builder.onBuildMethod(...).
            while (!methodBuilders.isEmpty()) {
                methodBuilders.iterator().next().build();
            }
        }

        private void processChildResourceBuilders() {
            // We have to iterate the set this way to prevent ConcurrentModificationExceptions
            // caused by the nested invocation of Set.remove(...) in Resource.Builder.onBuildChildResource(...).
            while (!childResourceBuilders.isEmpty()) {
                childResourceBuilders.iterator().next().build();
            }
        }

        @Override
        public String toString() {
            return "Builder{"
                    + "names=" + names
                    + ", path='" + path + '\''
                    + ", methodBuilders=" + methodBuilders
                    + ", childResourceBuilders=" + childResourceBuilders
                    + ", childResources=" + childResources
                    + ", resourceMethods=" + resourceMethods
                    + ", subResourceLocator=" + subResourceLocator
                    + ", handlerClasses=" + handlerClasses
                    + ", handlerInstances=" + handlerInstances
                    + ", parentResource=" + ((parentResource == null) ? "<no parent>" : parentResource.shortToString())
                    + ", extended=" + extended
                    + '}';
        }

        private String shortToString() {
            return "Builder{names=" + names + ", path='" + path + "'}";
        }
    }

    /**
     * Get a new unbound resource model builder.
     *
     * @return new unbound resource model builder.
     * @see Resource.Builder#path(java.lang.String)
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Get a new resource model builder for a resource bound to a given path.
     *
     * @param path resource path.
     * @return new resource model builder.
     * @see Resource.Builder#path(java.lang.String)
     */
    public static Builder builder(final String path) {
        return new Builder(path);
    }

    /**
     * Creates a {@link Builder resource builder} instance from the list of {@code resource} which can be merged
     * into a single resource. It must be possible to merge the {@code resources} into a single valid resource.
     * For example all resources must have the same {@link Resource#getPath() path}, they cannot have ambiguous methods
     * on the same path, etc.
     *
     * @param resources Resources with the same path.
     * @return Resource builder initialized from merged resources.
     */
    public static Builder builder(List<Resource> resources) {
        if (resources == null || resources.isEmpty()) {
            return builder();
        }

        final Iterator<Resource> it = resources.iterator();
        Resource.Data resourceData = it.next().data;
        Builder builder = Resource.builder(resourceData);
        String path = resourceData.path;

        while (it.hasNext()) {
            resourceData = it.next().data;
            if ((resourceData.path == null && path == null) || (path != null && path.equals(resourceData.path))) {
                builder.mergeWith(resourceData);
            } else {
                throw new IllegalArgumentException(LocalizationMessages.ERROR_RESOURCES_CANNOT_MERGE());
            }
        }

        return builder;
    }

    /**
     * Create a resource model builder initialized by introspecting an annotated
     * JAX-RS resource class.
     *
     * @param resourceClass resource class to be modelled.
     * @return resource model builder initialized by the class or {@code null} if the
     * class does not represent a resource.
     */
    public static Builder builder(Class<?> resourceClass) {
        return builder(resourceClass, false);
    }

    /**
     * Create a resource model builder initialized by introspecting an annotated
     * JAX-RS resource class.
     *
     * @param resourceClass     resource class to be modelled.
     * @param disableValidation if set to {@code true}, then any model validation checks will be disabled.
     * @return resource model builder initialized by the class or {@code null} if the
     * class does not represent a resource.
     */
    public static Builder builder(Class<?> resourceClass, boolean disableValidation) {
        final Builder builder = new IntrospectionModeller(resourceClass, disableValidation).createResourceBuilder();
        return builder.isEmpty() ? null : builder;
    }

    /**
     * Create a resource model initialized by introspecting an annotated
     * JAX-RS resource class.
     *
     * @param resourceClass resource class to be modelled.
     * @return resource model initialized by the class or {@code null} if the
     * class does not represent a resource.
     */
    public static Resource from(Class<?> resourceClass) {
        return from(resourceClass, false);
    }

    /**
     * Create a resource model initialized by introspecting an annotated
     * JAX-RS resource class.
     *
     * @param resourceClass     resource class to be modelled.
     * @param disableValidation if set to {@code true}, then any model validation checks will be disabled.
     * @return resource model initialized by the class or {@code null} if the
     * class does not represent a resource.
     */
    public static Resource from(Class<?> resourceClass, boolean disableValidation) {
        final Builder builder = new IntrospectionModeller(resourceClass, disableValidation).createResourceBuilder();
        return builder.isEmpty() ? null : builder.build();
    }

    /**
     * Check if the class is acceptable as a JAX-RS provider or resource.
     * <p/>
     * Method returns {@code false} if the class is either
     * <ul>
     * <li>abstract</li>
     * <li>interface</li>
     * <li>annotation</li>
     * <li>primitive</li>
     * <li>local class</li>
     * <li>non-static member class</li>
     * </ul>
     *
     * @param c class to be checked.
     * @return {@code true} if the class is an acceptable JAX-RS provider or
     * resource, {@code false} otherwise.
     */
    public static boolean isAcceptable(Class<?> c) {
        return !((c.getModifiers() & Modifier.ABSTRACT) != 0
                         || c.isPrimitive()
                         || c.isAnnotation()
                         || c.isInterface()
                         || c.isLocalClass()
                         || (c.isMemberClass() && (c.getModifiers() & Modifier.STATIC) == 0));
    }

    /**
     * Get the resource class {@link Path @Path} annotation.
     * <p/>
     * May return {@code null} in case there is no {@code @Path} annotation on the resource.
     *
     * @param resourceClass resource class.
     * @return {@code @Path} annotation instance if present on the resource class (i.e.
     * the class is a root resource class), or {@code null} otherwise.
     */
    public static Path getPath(Class<?> resourceClass) {
        return ModelHelper.getAnnotatedResourceClass(resourceClass).getAnnotation(Path.class);
    }

    /**
     * Get a new resource model builder initialized from a given resource model.
     *
     * @param resource resource model initializing the resource builder.
     * @return new resource model builder.
     */
    public static Builder builder(Resource resource) {
        return builder(resource.data);
    }

    private static Builder builder(Resource.Data resourceData) {
        final Builder b;
        if (resourceData.path == null) {
            b = new Builder();
        } else {
            b = new Builder(resourceData.path);
        }

        b.resourceMethods.addAll(resourceData.resourceMethods);
        b.childResources.addAll(resourceData.childResources);
        b.subResourceLocator = resourceData.subResourceLocator;

        b.handlerClasses.addAll(resourceData.handlerClasses);
        b.handlerInstances.addAll(resourceData.handlerInstances);

        b.names.addAll(resourceData.names);

        return b;
    }

    private static List<Resource> transform(final Resource parent, final List<Data> list) {
        return list.stream().map(data -> new Resource(parent, data)).collect(Collectors.toList());
    }

    private static <T> List<T> immutableCopy(List<T> list) {
        return list.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(list);
    }

    private static <T> Set<T> immutableCopy(Set<T> set) {
        if (set.isEmpty()) {
            return Collections.emptySet();
        }

        final Set<T> result = Collections.newSetFromMap(new IdentityHashMap<>());
        result.addAll(set);
        return set;
    }

    private final Resource parent;
    private final Data data;

    private final Value<String> name;

    private final List<ResourceMethod> resourceMethods;
    private final ResourceMethod locator;
    private final List<Resource> childResources;

    private Resource(final Resource parent, final Data data) {
        this.parent = parent;
        this.data = data;

        this.name = Values.lazy(new Value<String>() {
            @Override
            public String get() {
                if (data.names.size() == 1) {
                    return data.names.get(0);
                } else {
                    // return merged name
                    return "Merge of " + data.names.toString();
                }
            }
        });

        this.resourceMethods = immutableCopy(ResourceMethod.transform(Resource.this, data.resourceMethods));
        this.locator = data.subResourceLocator == null ? null : new ResourceMethod(Resource.this, data.subResourceLocator);
        this.childResources = immutableCopy(Resource.transform(Resource.this, data.childResources));
    }

    @Override
    public String getPath() {
        return data.path;
    }

    @Override
    public PathPattern getPathPattern() {
        return data.pathPattern;
    }

    /**
     * Get the parent resource for this resource model or {@code null} in case this
     * resource is a top-level resource and does not have a parent.
     *
     * @return parent resource or {@code null} if the resource does not have a parent.
     * @since 2.1
     */
    public Resource getParent() {
        return parent;
    }

    /**
     * Get the resource name.
     * <p/>
     * If the resource was constructed from a JAX-RS annotated resource class,
     * the resource name will be set to the {@link Class#getName() fully-qualified name}
     * of the resource class.
     *
     * @return reference JAX-RS resource handler class.
     */
    public String getName() {
        return name.get();
    }

    /**
     * Return a list of resource names.
     *
     * @return a list of resource names.
     */
    public List<String> getNames() {
        return data.names;
    }

    /**
     * Provides a non-null list of resource methods available on the resource.
     *
     * @return non-null abstract resource method list.
     */
    public List<ResourceMethod> getResourceMethods() {
        return resourceMethods;
    }

    /**
     * Provides a resource locator available on the resource.
     *
     * @return Resource locator if it is present, null otherwise.
     */
    public ResourceMethod getResourceLocator() {
        return locator;
    }

    /**
     * Provides resource methods and resource locator are available on the resource. The list is ordered so that resource
     * methods are positioned first before resource locator.
     *
     * @return List of resource methods and resource locator.
     */
    public List<ResourceMethod> getAllMethods() {
        final LinkedList<ResourceMethod> methodsAndLocators = new LinkedList<>(getResourceMethods());
        final ResourceMethod loc = getResourceLocator();
        if (loc != null) {
            methodsAndLocators.add(loc);
        }
        return methodsAndLocators;
    }

    /**
     * Returns the list of child resources available on this resource.
     *
     * @return Non-null list of child resources (may be empty).
     */
    public List<Resource> getChildResources() {
        return childResources;
    }

    /**
     * Get the method handler classes for the resource methods registered on the resource.
     *
     * @return resource method handler classes.
     */
    public Set<Class<?>> getHandlerClasses() {
        return data.handlerClasses;
    }

    /**
     * Get the method handler (singleton) instances for the resource methods registered
     * on the resource.
     *
     * @return resource method handler instances.
     */
    public Set<Object> getHandlerInstances() {
        return data.handlerInstances;
    }

    @Override
    public void accept(ResourceModelVisitor visitor) {
        if (getParent() == null) {
            visitor.visitResource(this);
        } else {
            visitor.visitChildResource(this);
        }
    }

    /**
     * Get the flag indicating whether the resource is extended.
     * <p/>
     * I.e. it is not part of the resource set that forms the REST API explicitly defined by the application developer (resource
     * classes and instances returned from the {@link javax.ws.rs.core.Application} subclass getters).
     * <p>
     * Extended resource model components are helper components that are not part of the explicitly defined REST API of
     * a JAX-RS application, instead they are generated by Jersey runtime. For example, extended resource model components
     * include {@code OPTIONS} {@link ResourceMethod resource methods}
     * automatically generated by Jersey {@link org.glassfish.jersey.server.model.ModelProcessor resource model processors}
     * or {@code application.wadl} resource API that exposes the application WADL descriptor.
     * </p>
     *
     * @return {@code true} if the resource is part of the application's extended REST API, {@code false} otherwise.
     * @see org.glassfish.jersey.server.model.ExtendedResource
     * @since 2.5.1
     */
    public boolean isExtended() {
        return data.extended;
    }

    @Override
    public String toString() {
        return data.toString();
    }

    @Override
    public List<? extends ResourceModelComponent> getComponents() {
        List<ResourceModelComponent> components = new LinkedList<>();

        components.addAll(getChildResources());
        components.addAll(getResourceMethods());

        final ResourceMethod resourceLocator = getResourceLocator();
        if (resourceLocator != null) {
            components.add(resourceLocator);
        }
        return components;
    }
}
