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.utils;
019
020import com.unboundid.scim2.common.Path;
021import com.unboundid.scim2.common.annotations.NotNull;
022import com.unboundid.scim2.common.annotations.Nullable;
023import com.unboundid.scim2.common.types.AttributeDefinition;
024import com.unboundid.scim2.common.types.ResourceTypeResource;
025import com.unboundid.scim2.common.types.SchemaResource;
026import com.unboundid.scim2.common.utils.SchemaUtils;
027import com.unboundid.scim2.server.annotations.ResourceType;
028
029import java.net.URI;
030import java.net.URISyntaxException;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039
040/**
041 * Declaration of a resource type including all schemas.
042 */
043public final class ResourceTypeDefinition
044{
045
046  @Nullable
047  private final String id;
048
049  @NotNull
050  private final String name;
051
052  @Nullable
053  private final String description;
054
055  @NotNull
056  private final String endpoint;
057
058  @Nullable
059  private final SchemaResource coreSchema;
060
061  @NotNull
062  private final Map<SchemaResource, Boolean> schemaExtensions;
063
064  @NotNull
065  private final Map<Path, AttributeDefinition> attributeNotationMap;
066
067  private final boolean discoverable;
068
069  /**
070   * Builder for creating a ResourceTypeDefinition.
071   */
072  public static class Builder
073  {
074    @NotNull
075    private final String name;
076
077    @NotNull
078    private final String endpoint;
079
080    @Nullable
081    private String id;
082
083    @Nullable
084    private String description;
085
086    @Nullable
087    private SchemaResource coreSchema;
088
089    @NotNull
090    private Set<SchemaResource> requiredSchemaExtensions =
091        new HashSet<SchemaResource>();
092
093    @NotNull
094    private Set<SchemaResource> optionalSchemaExtensions =
095        new HashSet<SchemaResource>();
096
097    private boolean discoverable = true;
098
099    /**
100     * Create a new builder.
101     *
102     * @param name The name of the resource type.
103     * @param endpoint The endpoint of the resource type.
104     */
105    public Builder(@NotNull final String name, @NotNull final String endpoint)
106    {
107      if(name == null)
108      {
109        throw new IllegalArgumentException("name must not be null");
110      }
111      if(endpoint == null)
112      {
113        throw new IllegalArgumentException("endpoint must not be null");
114      }
115      this.name = name;
116      this.endpoint = endpoint;
117    }
118
119    /**
120     * Sets the ID of the resource type.
121     *
122     * @param id the ID of the resource type.
123     * @return this builder.
124     */
125    @NotNull
126    public Builder setId(@Nullable final String id)
127    {
128      this.id = id;
129      return this;
130    }
131
132    /**
133     * Sets the description of the resource type.
134     *
135     * @param description the description of the resource type.
136     * @return this builder.
137     */
138    @NotNull
139    public Builder setDescription(@Nullable final String description)
140    {
141      this.description = description;
142      return this;
143    }
144
145    /**
146     * Sets the core schema of the resource type.
147     *
148     * @param coreSchema the core schema of the resource type.
149     * @return this builder.
150     */
151    @NotNull
152    public Builder setCoreSchema(@Nullable final SchemaResource coreSchema)
153    {
154      this.coreSchema = coreSchema;
155      return this;
156    }
157
158    /**
159     * Adds a required schema extension for a resource type.
160     *
161     * @param schemaExtension the required schema extension for the resource
162     *                        type.
163     * @return this builder.
164     */
165    @NotNull
166    public Builder addRequiredSchemaExtension(
167        @NotNull final SchemaResource schemaExtension)
168    {
169      this.requiredSchemaExtensions.add(schemaExtension);
170      return this;
171    }
172
173    /**
174     * Adds a operation schema extension for a resource type.
175     *
176     * @param schemaExtension the operation schema extension for the resource
177     *                        type.
178     * @return this builder.
179     */
180    @NotNull
181    public Builder addOptionalSchemaExtension(
182        @NotNull final SchemaResource schemaExtension)
183    {
184      this.optionalSchemaExtensions.add(schemaExtension);
185      return this;
186    }
187
188    /**
189     * Sets whether this resource type is discoverable over the /ResourceTypes
190     * endpoint.
191     *
192     * @param discoverable {@code true} this resource type is discoverable over
193     *                     the /ResourceTypes endpoint or {@code false}
194     *                     otherwise.
195     * @return this builder.
196     */
197    @NotNull
198    public Builder setDiscoverable(final boolean discoverable)
199    {
200      this.discoverable = discoverable;
201      return this;
202    }
203
204    /**
205     * Build the ResourceTypeDefinition.
206     *
207     * @return The newly created ResourceTypeDefinition.
208     */
209    @NotNull
210    public ResourceTypeDefinition build()
211    {
212      Map<SchemaResource, Boolean> schemaExtensions =
213          new HashMap<SchemaResource, Boolean>(requiredSchemaExtensions.size() +
214              optionalSchemaExtensions.size());
215      for(SchemaResource schema : requiredSchemaExtensions)
216      {
217        schemaExtensions.put(schema, true);
218      }
219      for(SchemaResource schema : optionalSchemaExtensions)
220      {
221        schemaExtensions.put(schema, false);
222      }
223      return new ResourceTypeDefinition(id, name, description, endpoint,
224          coreSchema, schemaExtensions, discoverable);
225    }
226  }
227
228  /**
229   * Create a new ResourceType.
230   *
231   * @param coreSchema The core schema for the resource type.
232   * @param schemaExtensions A map of schema extensions to whether it is
233   *                         required for the resource type.
234   */
235  private ResourceTypeDefinition(
236      @Nullable final String id,
237      @NotNull final String name,
238      @Nullable final String description,
239      @NotNull final String endpoint,
240      @Nullable final SchemaResource coreSchema,
241      @NotNull final Map<SchemaResource, Boolean> schemaExtensions,
242      final boolean discoverable)
243  {
244    this.id = id;
245    this.name = name;
246    this.description = description;
247    this.endpoint = endpoint;
248    this.coreSchema = coreSchema;
249    this.schemaExtensions = Collections.unmodifiableMap(schemaExtensions);
250    this.discoverable = discoverable;
251    this.attributeNotationMap = new HashMap<Path, AttributeDefinition>();
252
253    // Add the common attributes
254    buildAttributeNotationMap(Path.root(),
255        SchemaUtils.COMMON_ATTRIBUTE_DEFINITIONS);
256
257    // Add the core attributes
258    if(coreSchema != null)
259    {
260      buildAttributeNotationMap(Path.root(), coreSchema.getAttributes());
261    }
262
263    // Add the extension attributes
264    for(SchemaResource schemaExtension : schemaExtensions.keySet())
265    {
266      buildAttributeNotationMap(Path.root(schemaExtension.getId()),
267          schemaExtension.getAttributes());
268    }
269  }
270
271  private void buildAttributeNotationMap(
272      @NotNull final Path parentPath,
273      @NotNull final Collection<AttributeDefinition> attributes)
274  {
275    for(AttributeDefinition attribute : attributes)
276    {
277      Path path = parentPath.attribute(attribute.getName());
278      attributeNotationMap.put(path, attribute);
279      if(attribute.getSubAttributes() != null)
280      {
281        buildAttributeNotationMap(path, attribute.getSubAttributes());
282      }
283    }
284  }
285
286  /**
287   * Gets the resource type name.
288   *
289   * @return the name of the resource type.
290   */
291  @NotNull
292  public String getName()
293  {
294    return name;
295  }
296
297  /**
298   * Gets the description of the resource type.
299   *
300   * @return the description of the resource type.
301   */
302  @Nullable
303  public String getDescription()
304  {
305    return description;
306  }
307
308  /**
309   * Gets the resource type's endpoint.
310   *
311   * @return the endpoint for the resource type.
312   */
313  @NotNull
314  public String getEndpoint()
315  {
316    return endpoint;
317  }
318
319  /**
320   * Gets the resource type's schema.
321   *
322   * @return the schema for the resource type.
323   */
324  @Nullable
325  public SchemaResource getCoreSchema()
326  {
327    return coreSchema;
328  }
329
330  /**
331   * Gets the resource type's schema extensions.
332   *
333   * @return the schema extensions for the resource type.
334   */
335  @NotNull
336  public Map<SchemaResource, Boolean> getSchemaExtensions()
337  {
338    return schemaExtensions;
339  }
340
341  /**
342   * Whether this resource type and its associated schemas should be
343   * discoverable using the SCIM 2 standard /resourceTypes and /schemas
344   * endpoints.
345   *
346   * @return {@code true} if discoverable or {@code false} otherwise.
347   */
348  public boolean isDiscoverable()
349  {
350    return discoverable;
351  }
352
353  /**
354   * Retrieve the attribute definition for the attribute in the path.
355   *
356   * @param path The attribute path.
357   * @return The attribute definition or {@code null} if there is no attribute
358   * defined for the path.
359   */
360  @Nullable
361  public AttributeDefinition getAttributeDefinition(@NotNull final Path path)
362  {
363    return attributeNotationMap.get(normalizePath(path).withoutFilters());
364  }
365
366  /**
367   * Normalize a path by removing the schema URN for core attributes.
368   *
369   * @param path The path to normalize.
370   * @return The normalized path.
371   */
372  @NotNull
373  public Path normalizePath(@NotNull final Path path)
374  {
375    if(path.getSchemaUrn() != null && coreSchema != null &&
376        path.getSchemaUrn().equalsIgnoreCase(coreSchema.getId()))
377    {
378      return Path.root().attribute(path);
379    }
380    return path;
381  }
382
383  /**
384   * Retrieve the ResourceType SCIM resource that represents this definition.
385   *
386   * @return The ResourceType SCIM resource that represents this definition.
387   */
388  @NotNull
389  public ResourceTypeResource toScimResource()
390  {
391    try
392    {
393      URI coreSchemaUri = null;
394      if(coreSchema != null)
395      {
396        coreSchemaUri = new URI(coreSchema.getId());
397      }
398      List<ResourceTypeResource.SchemaExtension> schemaExtensionList = null;
399      if (schemaExtensions.size() > 0)
400      {
401        schemaExtensionList =
402            new ArrayList<ResourceTypeResource.SchemaExtension>(
403                schemaExtensions.size());
404
405        for(Map.Entry<SchemaResource, Boolean> schemaExtension :
406            schemaExtensions.entrySet())
407        {
408          schemaExtensionList.add(new ResourceTypeResource.SchemaExtension(
409              URI.create(schemaExtension.getKey().getId()),
410              schemaExtension.getValue()));
411        }
412      }
413
414      return new ResourceTypeResource(id == null ? name : id, name, description,
415          URI.create(endpoint), coreSchemaUri, schemaExtensionList);
416    }
417    catch(URISyntaxException e)
418    {
419      throw new RuntimeException(e);
420    }
421  }
422
423  /**
424   * Create a new instance representing the resource type implemented by a
425   * root JAX-RS resource class.
426   *
427   * @param resource a root resource whose
428   *                 {@link com.unboundid.scim2.server.annotations.ResourceType}
429   *                 and {@link jakarta.ws.rs.Path} values will be used to
430   *                 initialize the ResourceTypeDefinition.
431   * @return a new ResourceTypeDefinition or {@code null} if resource is not
432   * annotated with {@link com.unboundid.scim2.server.annotations.ResourceType}
433   * and {@link jakarta.ws.rs.Path}.
434   */
435  @Nullable
436  public static ResourceTypeDefinition fromJaxRsResource(
437      @NotNull final Class<?> resource)
438  {
439    Class<?> c = resource;
440    ResourceType resourceType;
441    do
442    {
443      resourceType = c.getAnnotation(ResourceType.class);
444      c = c.getSuperclass();
445    }
446    while (c != null && resourceType == null);
447
448    c = resource;
449    jakarta.ws.rs.Path path;
450    do
451    {
452      path = c.getAnnotation(jakarta.ws.rs.Path.class);
453      c = c.getSuperclass();
454    }
455    while (c != null && path == null);
456
457    if (resourceType == null || path == null)
458    {
459      return null;
460    }
461
462    try
463    {
464      ResourceTypeDefinition.Builder builder =
465          new Builder(resourceType.name(), path.value());
466      builder.setDescription(resourceType.description());
467      builder.setCoreSchema(SchemaUtils.getSchema(resourceType.schema()));
468      builder.setDiscoverable(
469          resourceType.discoverable());
470
471      for (Class<?> optionalSchemaExtension :
472          resourceType.optionalSchemaExtensions())
473      {
474        builder.addOptionalSchemaExtension(
475            SchemaUtils.getSchema(optionalSchemaExtension));
476      }
477
478      for (Class<?> requiredSchemaExtension :
479          resourceType.requiredSchemaExtensions())
480      {
481        builder.addRequiredSchemaExtension(
482            SchemaUtils.getSchema(requiredSchemaExtension));
483      }
484
485      return builder.build();
486    }
487    catch (Exception e)
488    {
489      throw new IllegalArgumentException(e);
490    }
491  }
492}