<commit_before>package net.oneandone.concierge.resource;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import net.oneandone.concierge.api.Element;
import net.oneandone.concierge.api.Extension;
import net.oneandone.concierge.api.Group;
import net.oneandone.concierge.api.filter.AddressFilter;
import net.oneandone.concierge.api.filter.Filter;
import net.oneandone.concierge.api.filter.Filters;
import net.oneandone.concierge.api.filter.PageFilter;
import net.oneandone.concierge.api.resolver.ExtensionResolver;
import net.oneandone.concierge.api.resolver.GroupResolver;
import net.oneandone.concierge.resource.response.ApiResourcePaging;
import net.oneandone.concierge.resource.response.ApiResponse;

import javax.json.*;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class GenericApiResource {

    private List<ExtensionResolver> extensionResolvers = new ArrayList<>();
    private List<GroupResolver> groupResolvers = new ArrayList<>();

    public GenericApiResource(final List<GroupResolver> groupResolvers, final List<ExtensionResolver> extensionResolvers) {
        Preconditions.checkNotNull(groupResolvers, "the group resolvers may not be null");
        Preconditions.checkNotNull(extensionResolvers, "the extension resolvers may not be null");

        this.groupResolvers = groupResolvers;
        this.extensionResolvers = extensionResolvers;
    }

    @GET
    @Path("/{uri:.*}")
    public Response getResource(@PathParam("uri") String uri,
                                @QueryParam("page") Integer page,
                                @QueryParam("per_page") Integer perPage,
                                @QueryParam("show") List<String> show) {
        final ApiResponse apiResponse = getResponse(uri, Optional.ofNullable(page), Optional.ofNullable(perPage), show.toArray(new String[show.size()]));
        if (apiResponse == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        final JsonStructure jsonResponse = apiResponse.getObject();
        if (jsonResponse != null) {
            final Response.ResponseBuilder responseBuilder = Response.ok(jsonResponse.toString());
            if (apiResponse.getPaging().isPresent()) {
                responseBuilder.header("Accept-Ranges", apiResponse.getPaging().get().getAcceptRanges());
                responseBuilder.header("Content-Range", apiResponse.getPaging().get().getContentRange());
            }
            responseBuilder.header("Last-Modified", apiResponse.getLastModified().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
            return responseBuilder.build();
        }
        return Response.serverError().entity("nothing found for " + uri).build();
    }

    /**
     * Creates a response for the specified url.
     *
     * @param url        the URL
     * @param page       the optional page
     * @param perPage    the optional per page limit
     * @param extensions an array of all requested extensions
     * @return the response or null
     */
    private ApiResponse getResponse(final String url, final Optional<Integer> page, final Optional<Integer> perPage, final String... extensions) {
        Preconditions.checkNotNull(url, "the URI may not be null");
        Preconditions.checkArgument(!url.startsWith("/"), "the URI may not start with slash");

        final String[] urlParts = url.split("/");
        return getResponse(urlParts, null, page, perPage, extensions);
    }

    /**
     * Returns the JSON representation for the specified {@code addresses} wrapped in a {@link ApiResponse} or {@code null} if no resource could be found for the specified address.
     *
     * @param addresses  the array of addresses
     * @param parent     the parent element or {@code null} at the root of the resource graph
     * @param page       the optional page
     * @param perPage    the optional per page limit
     * @param extensions an array of all requested extensions
     * @return the response or {@code null}
     */
    private ApiResponse getResponse(final String[] addresses, final Element parent, Optional<Integer> page, Optional<Integer> perPage, final String... extensions) {
        Preconditions.checkNotNull(addresses, "the addresses may not be null");
        Preconditions.checkArgument(addresses.length > 0, "the addresses may not be empty");

        final String groupName = addresses[0];
        final Optional<GroupResolver> resolver = groupResolvers.stream().filter(r -> r.name().equals(groupName)).findAny();

        if (resolver.isPresent()) {
            return getGroupResponse(addresses, parent, page, perPage, groupName, resolver.get(), extensions);
        } else {
            final Optional<ExtensionResolver> extensionResolver = extensionResolvers.stream().filter(r -> r.name().equals(groupName)).findAny();
            if (parent != null && extensionResolver.isPresent()) {
                final Optional<Extension> extension = extensionResolver.get().resolve(parent);
                if (extension.isPresent()) {
                    return ApiResponse.create(extension.get().content(), extension.get().lastModified());
                }
                return null;
            } else {
                return null;
            }
        }
    }

    /**
     * Returns the response for a {@link Group} or {@link Element}.
     *
     * @param addresses  the array of addresses
     * @param parent     the parent element or {@code null} at the root of the resource graph
     * @param page       the optional page
     * @param perPage    the optional per page limit
     * @param groupName  the group name within the selection
     * @param resolver   the resolver for the group with the specified {@code groupName}
     * @param extensions an array of all requested extensions
     * @return the response
     */
    private ApiResponse getGroupResponse(String[] addresses, Element parent, Optional<Integer> page, Optional<Integer> perPage, String groupName, GroupResolver resolver, String... extensions) {
        // Unique by URL concept: group/element/group/element/...
        final boolean uniqueElement = addresses.length >= 2;

        final Filters filters = initializeFilters(addresses, page, perPage);
        final Group group = resolver.elements(parent, filters);

        final Multimap<Element, Extension> extensionMultimap = HashMultimap.create();
        for (final String extension : extensions) {
            final List<ExtensionResolver> extensionResolvers = this.extensionResolvers.stream().filter(e -> e.name().equals(extension) && e.forGroup().equals(addresses[0])).collect(Collectors.toList());
            for (final ExtensionResolver extensionResolver : extensionResolvers) {
                final Map<Element, Extension> resolvedExtensions = extensionResolver.resolve(group);
                for (final Map.Entry<Element, Extension> elementExtensionEntry : resolvedExtensions.entrySet()) {
                    extensionMultimap.put(elementExtensionEntry.getKey(), elementExtensionEntry.getValue());
                }
            }
        }

        final Optional<String[]> nextUrl;
        if (addresses.length > 2) {
            nextUrl = Optional.of(Arrays.copyOfRange(addresses, 2, addresses.length));
        } else {
            nextUrl = Optional.empty();
        }

        if (uniqueElement) {
            if (group != null && !group.elements().isEmpty()) {
                final Element element = group.elements().get(0);
                return ApiResponse.create(getExtendedJsonStructure(nextUrl, element, extensions, extensionMultimap.get(element)), element.lastModified());
            } else {
                return null;
            }
        } else {
            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
            for (final Element element : group.elements()) {
                arrayBuilder.add(getExtendedJsonStructure(nextUrl, element, extensions, extensionMultimap.get(element)));
            }

            if (resolver.defaultPageSize() > 0) {
                final ApiResourcePaging paging = ApiResourcePaging.builder().group(groupName).page(page.orElse(1)).perPage(perPage.orElse(resolver.defaultPageSize())).total(group.total()).build();
                return ApiResponse.create(arrayBuilder.build(), group.lastModified(), paging);
            }
            return ApiResponse.create(arrayBuilder.build(), group.lastModified());
        }
    }

    /**
     * Returns the {@link JsonStructure} for the parent element if {@code nextUrl} is empty or else forwards the request to
     * {@link #getResponse(String[], Element, Optional, Optional, String...)} with the current parent element and selects recursively the result of it.
     *
     * @param nextUrl            the optional nexty url
     * @param parent             the parent element
     * @param extensions         an array of all requested extensions
     * @param resolvedExtensions a list of all resolved extensions so far
     * @return the JSON representation for the parent element or the result of a forwarded request
     */
    private JsonStructure getExtendedJsonStructure(final Optional<String[]> nextUrl, final Element parent, String[] extensions, final Collection<Extension> resolvedExtensions) {
        if (nextUrl.isPresent()) {
            return getResponse(nextUrl.get(), parent, Optional.empty(), Optional.empty(), extensions).getObject();
        } else {
            final JsonObjectBuilder objectBuilder = Json.createObjectBuilder();

            final JsonObject jsonObject = parent.content();
            for (final String key : jsonObject.keySet()) {
                objectBuilder.add(key, jsonObject.get(key));
            }

            for (final Extension extension : resolvedExtensions) {
                objectBuilder.add(extension.address(), extension.content());
            }

            for (final String extension : extensions) {
                final Optional<GroupResolver> resolver = groupResolvers.stream().filter(r -> r.name().equals(extension) && r.parentGroup().isPresent() && parent.group().equals(r.parentGroup().get())).findAny();
                if (resolver.isPresent()) {
                    final ApiResponse apiResponse = getResponse(new String[]{extension}, parent, Optional.empty(), Optional.empty(), extensions);
                    if (apiResponse != null) {
                        objectBuilder.add(extension, apiResponse.getObject());
                    }
                }
            }

            return objectBuilder.build();
        }
    }

    /**
     * Returns a list of all {@link Filter} for the specified {@code resolver}.
     *<commit_msg>removed next uri optional<commit_after>package net.oneandone.concierge.resource;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import net.oneandone.concierge.api.Element;
import net.oneandone.concierge.api.Extension;
import net.oneandone.concierge.api.Group;
import net.oneandone.concierge.api.filter.AddressFilter;
import net.oneandone.concierge.api.filter.Filter;
import net.oneandone.concierge.api.filter.Filters;
import net.oneandone.concierge.api.filter.PageFilter;
import net.oneandone.concierge.api.resolver.ExtensionResolver;
import net.oneandone.concierge.api.resolver.GroupResolver;
import net.oneandone.concierge.resource.response.ApiResourcePaging;
import net.oneandone.concierge.resource.response.ApiResponse;

import javax.json.*;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class GenericApiResource {

    private List<ExtensionResolver> extensionResolvers = new ArrayList<>();
    private List<GroupResolver> groupResolvers = new ArrayList<>();

    public GenericApiResource(final List<GroupResolver> groupResolvers, final List<ExtensionResolver> extensionResolvers) {
        Preconditions.checkNotNull(groupResolvers, "the group resolvers may not be null");
        Preconditions.checkNotNull(extensionResolvers, "the extension resolvers may not be null");

        this.groupResolvers = groupResolvers;
        this.extensionResolvers = extensionResolvers;
    }

    @GET
    @Path("/{uri:.*}")
    public Response getResource(@PathParam("uri") String uri,
                                @QueryParam("page") Integer page,
                                @QueryParam("per_page") Integer perPage,
                                @QueryParam("show") List<String> show) {
        final ApiResponse apiResponse = getResponse(uri, Optional.ofNullable(page), Optional.ofNullable(perPage), show.toArray(new String[show.size()]));
        if (apiResponse == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        final JsonStructure jsonResponse = apiResponse.getObject();
        if (jsonResponse != null) {
            final Response.ResponseBuilder responseBuilder = Response.ok(jsonResponse.toString());
            if (apiResponse.getPaging().isPresent()) {
                responseBuilder.header("Accept-Ranges", apiResponse.getPaging().get().getAcceptRanges());
                responseBuilder.header("Content-Range", apiResponse.getPaging().get().getContentRange());
            }
            responseBuilder.header("Last-Modified", apiResponse.getLastModified().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
            return responseBuilder.build();
        }
        return Response.serverError().entity("nothing found for " + uri).build();
    }

    /**
     * Creates a response for the specified uri.
     *
     * @param uri        the URI
     * @param page       the optional page
     * @param perPage    the optional per page limit
     * @param extensions an array of all requested extensions
     * @return the response or null
     */
    private ApiResponse getResponse(final String uri, final Optional<Integer> page, final Optional<Integer> perPage, final String... extensions) {
        Preconditions.checkNotNull(uri, "the URI may not be null");
        Preconditions.checkArgument(!uri.startsWith("/"), "the URI may not start with slash");

        final String[] urlParts = uri.split("/");
        return getResponse(urlParts, null, page, perPage, extensions);
    }

    /**
     * Returns the JSON representation for the specified {@code addresses} wrapped in a {@link ApiResponse} or {@code null} if no resource could be found for the specified address.
     *
     * @param addresses  the array of addresses
     * @param parent     the parent element or {@code null} at the root of the resource graph
     * @param page       the optional page
     * @param perPage    the optional per page limit
     * @param extensions an array of all requested extensions
     * @return the response or {@code null}
     */
    private ApiResponse getResponse(final String[] addresses, final Element parent, Optional<Integer> page, Optional<Integer> perPage, final String... extensions) {
        Preconditions.checkNotNull(addresses, "the addresses may not be null");
        Preconditions.checkArgument(addresses.length > 0, "the addresses may not be empty");

        final String groupName = addresses[0];
        final Optional<GroupResolver> resolver = groupResolvers.stream().filter(r -> r.name().equals(groupName)).findAny();

        if (resolver.isPresent()) {
            return getGroupResponse(addresses, parent, page, perPage, groupName, resolver.get(), extensions);
        } else {
            final Optional<ExtensionResolver> extensionResolver = extensionResolvers.stream().filter(r -> r.name().equals(groupName)).findAny();
            if (parent != null && extensionResolver.isPresent()) {
                final Optional<Extension> extension = extensionResolver.get().resolve(parent);
                if (extension.isPresent()) {
                    return ApiResponse.create(extension.get().content(), extension.get().lastModified());
                }
                return null;
            } else {
                return null;
            }
        }
    }

    /**
     * Returns the response for a {@link Group} or {@link Element}.
     *
     * @param addresses  the array of addresses
     * @param parent     the parent element or {@code null} at the root of the resource graph
     * @param page       the optional page
     * @param perPage    the optional per page limit
     * @param groupName  the group name within the selection
     * @param resolver   the resolver for the group with the specified {@code groupName}
     * @param extensions an array of all requested extensions
     * @return the response
     */
    private ApiResponse getGroupResponse(String[] addresses, Element parent, Optional<Integer> page, Optional<Integer> perPage, String groupName, GroupResolver resolver, String... extensions) {
        // Unique by URL concept: group/element/group/element/...
        final boolean uniqueElement = addresses.length >= 2;

        final Filters filters = initializeFilters(addresses, page, perPage);
        final Group group = resolver.elements(parent, filters);

        final Multimap<Element, Extension> extensionMultimap = HashMultimap.create();
        for (final String extension : extensions) {
            final List<ExtensionResolver> extensionResolvers = this.extensionResolvers.stream().filter(e -> e.name().equals(extension) && e.forGroup().equals(addresses[0])).collect(Collectors.toList());
            for (final ExtensionResolver extensionResolver : extensionResolvers) {
                final Map<Element, Extension> resolvedExtensions = extensionResolver.resolve(group);
                for (final Map.Entry<Element, Extension> elementExtensionEntry : resolvedExtensions.entrySet()) {
                    extensionMultimap.put(elementExtensionEntry.getKey(), elementExtensionEntry.getValue());
                }
            }
        }

        final String[] nextUri;
        if (addresses.length > 2) {
            nextUri = Arrays.copyOfRange(addresses, 2, addresses.length);
        } else {
            nextUri = new String[0];
        }

        if (uniqueElement) {
            if (group != null && !group.elements().isEmpty()) {
                final Element element = group.elements().get(0);
                return ApiResponse.create(getExtendedJsonStructure(nextUri, element, extensions, extensionMultimap.get(element)), element.lastModified());
            } else {
                return null;
            }
        } else {
            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
            for (final Element element : group.elements()) {
                arrayBuilder.add(getExtendedJsonStructure(nextUri, element, extensions, extensionMultimap.get(element)));
            }

            if (resolver.defaultPageSize() > 0) {
                final ApiResourcePaging paging = ApiResourcePaging.builder().group(groupName).page(page.orElse(1)).perPage(perPage.orElse(resolver.defaultPageSize())).total(group.total()).build();
                return ApiResponse.create(arrayBuilder.build(), group.lastModified(), paging);
            }
            return ApiResponse.create(arrayBuilder.build(), group.lastModified());
        }
    }

    /**
     * Returns the {@link JsonStructure} for the parent element if {@code nextUri} is empty or else forwards the request to
     * {@link #getResponse(String[], Element, Optional, Optional, String...)} with the current parent element and selects recursively the result of it.
     *
     * @param nextUri            the optional next uri
     * @param parent             the parent element
     * @param extensions         an array of all requested extensions
     * @param resolvedExtensions a list of all resolved extensions so far
     * @return the JSON representation for the parent element or the result of a forwarded request
     */
    private JsonStructure getExtendedJsonStructure(final String[] nextUri, final Element parent, String[] extensions, final Collection<Extension> resolvedExtensions) {
        if (nextUri.length > 0) {
            return getResponse(nextUri, parent, Optional.empty(), Optional.empty(), extensions).getObject();
        } else {
            final JsonObjectBuilder objectBuilder = Json.createObjectBuilder();

            final JsonObject jsonObject = parent.content();
            for (final String key : jsonObject.keySet()) {
                objectBuilder.add(key, jsonObject.get(key));
            }

            for (final Extension extension : resolvedExtensions) {
                objectBuilder.add(extension.address(), extension.content());
            }

            for (final String extension : extensions) {
                final Optional<GroupResolver> resolver = groupResolvers.stream().filter(r -> r.name().equals(extension) && r.parentGroup().isPresent() && parent.group().equals(r.parentGroup().get())).findAny();
                if (resolver.isPresent()) {
                    final ApiResponse apiResponse = getResponse(new String[]{extension}, parent, Optional.empty(), Optional.empty(), extensions);
                    if (apiResponse != null) {
                        objectBuilder.add(extension, apiResponse.getObject());
                    }
                }
            }

            return objectBuilder.build();
        }
    }

    /**
     * Returns a list of all {@link Filter} for the specified {@code resolver}.
     *
{'fraction_non_alphanumeric': 0.10537830446672744, 'fraction_numerical': 0.0009115770282588879, 'mean_word_length': 2.849298245614035, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 66, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7060767', 'n_tokens_mistral': 5487, 'n_tokens_neox': 5383, 'n_words': 1477}