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