001/*
002 * Copyright 2015-2024 Ping Identity Corporation
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim2.server.resources;
019
020import com.unboundid.scim2.common.GenericScimResource;
021import com.unboundid.scim2.common.ScimResource;
022import com.unboundid.scim2.common.annotations.NotNull;
023import com.unboundid.scim2.common.annotations.Nullable;
024import com.unboundid.scim2.common.filters.Filter;
025import com.unboundid.scim2.common.messages.ListResponse;
026import com.unboundid.scim2.common.types.SchemaResource;
027import com.unboundid.scim2.common.exceptions.ForbiddenException;
028import com.unboundid.scim2.common.exceptions.ResourceNotFoundException;
029import com.unboundid.scim2.common.exceptions.ScimException;
030import com.unboundid.scim2.server.annotations.ResourceType;
031import com.unboundid.scim2.server.utils.ResourcePreparer;
032import com.unboundid.scim2.server.utils.ResourceTypeDefinition;
033import com.unboundid.scim2.server.utils.SchemaAwareFilterEvaluator;
034
035import jakarta.ws.rs.GET;
036import jakarta.ws.rs.Path;
037import jakarta.ws.rs.PathParam;
038import jakarta.ws.rs.Produces;
039import jakarta.ws.rs.QueryParam;
040import jakarta.ws.rs.core.Application;
041import jakarta.ws.rs.core.Context;
042import jakarta.ws.rs.core.MediaType;
043import jakarta.ws.rs.core.UriInfo;
044
045import java.util.ArrayList;
046import java.util.Collection;
047import java.util.HashSet;
048import java.util.Set;
049
050import static com.unboundid.scim2.common.utils.ApiConstants.*;
051
052/**
053 * An abstract JAX-RS resource class for servicing the Schemas
054 * endpoint.
055 */
056@ResourceType(
057    description = "SCIM 2.0 Schema",
058    name = "Schema",
059    schema = SchemaResource.class,
060    discoverable = false)
061@Path("Schemas")
062public class SchemasEndpoint
063{
064  @NotNull
065  private static final ResourceTypeDefinition RESOURCE_TYPE_DEFINITION =
066      ResourceTypeDefinition.fromJaxRsResource(
067          SchemasEndpoint.class);
068
069  @Nullable
070  @Context
071  private Application application;
072
073  /**
074   * Service SCIM request to retrieve all schemas defined at the
075   * service provider using GET.
076   *
077   * @param filterString The filter string used to request a subset of
078   *                     resources. Will throw 403 Forbidden if specified.
079   * @param uriInfo UriInfo of the request.
080   * @return All schemas in a ListResponse container.
081   * @throws ScimException If an error occurs.
082   */
083  @NotNull
084  @GET
085  @Produces({MEDIA_TYPE_SCIM, MediaType.APPLICATION_JSON})
086  public ListResponse<GenericScimResource> search(
087      @Nullable @QueryParam(QUERY_PARAMETER_FILTER) final String filterString,
088      @NotNull @Context final UriInfo uriInfo)
089          throws ScimException
090  {
091    if(filterString != null)
092    {
093      throw new ForbiddenException("Filtering not allowed");
094    }
095
096    // https://tools.ietf.org/html/draft-ietf-scim-api-19#section-4 says
097    // query params should be ignored for discovery endpoints so we can't use
098    // SimpleSearchResults.
099    ResourcePreparer<GenericScimResource> preparer =
100        new ResourcePreparer<GenericScimResource>(
101            RESOURCE_TYPE_DEFINITION, uriInfo);
102    Collection<SchemaResource> schemas = getSchemas();
103    Collection<GenericScimResource> preparedResources =
104        new ArrayList<GenericScimResource>(schemas.size());
105    for(SchemaResource schema : schemas)
106    {
107      GenericScimResource preparedResource = schema.asGenericScimResource();
108      preparer.setResourceTypeAndLocation(preparedResource);
109      preparedResources.add(preparedResource);
110    }
111    return new ListResponse<GenericScimResource>(preparedResources);
112  }
113
114  /**
115   * Service SCIM request to retrieve a schema by ID.
116   *
117   * @param id The ID of the schema to retrieve.
118   * @param uriInfo UriInfo of the request.
119   * @return The retrieved schema.
120   * @throws ScimException If an error occurs.
121   */
122  @Path("{id}")
123  @GET
124  @Produces({MEDIA_TYPE_SCIM, MediaType.APPLICATION_JSON})
125  @NotNull
126  public ScimResource get(@NotNull @PathParam("id") final String id,
127                          @NotNull @Context final UriInfo uriInfo)
128      throws ScimException
129  {
130    Filter filter = Filter.or(Filter.eq("id", id), Filter.eq("name", id));
131    SchemaAwareFilterEvaluator filterEvaluator =
132        new SchemaAwareFilterEvaluator(RESOURCE_TYPE_DEFINITION);
133    ResourcePreparer<GenericScimResource> resourcePreparer =
134        new ResourcePreparer<GenericScimResource>(
135            RESOURCE_TYPE_DEFINITION, uriInfo);
136    for (SchemaResource schema : getSchemas())
137    {
138      GenericScimResource resource = schema.asGenericScimResource();
139      if (filter.visit(filterEvaluator, resource.getObjectNode()))
140      {
141        resourcePreparer.setResourceTypeAndLocation(resource);
142        return resource;
143      }
144    }
145
146    throw new ResourceNotFoundException("No schema defined with ID " + id);
147  }
148
149  /**
150   * Retrieve all schemas defined at the service provider. The default
151   * implementation will generate Schemas definitions based on the ResourceType
152   * of all JAX-RS resource classes with the ResourceType annotation.
153   *
154   * @return All schemas defined at the service provider.
155   * @throws ScimException If an error occurs.
156   */
157  @NotNull
158  public Collection<SchemaResource> getSchemas() throws ScimException
159  {
160    Set<SchemaResource> schemas =
161        new HashSet<SchemaResource>();
162    for(Class<?> resourceClass : application.getClasses())
163    {
164      ResourceTypeDefinition resourceTypeDefinition =
165          ResourceTypeDefinition.fromJaxRsResource(resourceClass);
166      if(resourceTypeDefinition != null &&
167          resourceTypeDefinition.isDiscoverable())
168      {
169        if(resourceTypeDefinition.getCoreSchema() != null)
170        {
171          schemas.add(resourceTypeDefinition.getCoreSchema());
172        }
173        for(SchemaResource schemaExtension :
174            resourceTypeDefinition.getSchemaExtensions().keySet())
175        {
176          schemas.add(schemaExtension);
177        }
178      }
179    }
180    for(Object resourceInstance : application.getSingletons())
181    {
182      ResourceTypeDefinition resourceTypeDefinition =
183          ResourceTypeDefinition.fromJaxRsResource(resourceInstance.getClass());
184      if(resourceTypeDefinition != null &&
185          resourceTypeDefinition.isDiscoverable())
186      {
187        if(resourceTypeDefinition.getCoreSchema() != null)
188        {
189          schemas.add(resourceTypeDefinition.getCoreSchema());
190        }
191        for(SchemaResource schemaExtension :
192            resourceTypeDefinition.getSchemaExtensions().keySet())
193        {
194          schemas.add(schemaExtension);
195        }
196      }
197    }
198
199    return schemas;
200  }
201}