001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016
017package org.forgerock.api.util;
018
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023
024import org.forgerock.api.models.ApiDescription;
025import org.forgerock.api.models.ApiError;
026import org.forgerock.api.models.Reference;
027import org.forgerock.api.models.Resource;
028import org.forgerock.api.models.Schema;
029import org.forgerock.util.Reject;
030
031/**
032 * Helper that registers one or more {@link ApiDescription} instances and provides a means to resolve
033 * {@link Reference}s.
034 */
035public class ReferenceResolver {
036
037    private static final String DEFINITIONS_REF = "#/definitions/";
038
039    private static final String ERRORS_REF = "#/errors/";
040
041    private static final String SERVICES_REF = "#/services/";
042
043    private final ApiDescription local;
044
045    private final Map<String, ApiDescription> map;
046
047    /**
048     * Creates a reference-resolver and defines the one {@link ApiDescription} that can be used for local
049     * (non-namespaced) reference lookups.
050     *
051     * @param local {@link ApiDescription} to use for local (non-namespaced) reference lookups
052     */
053    public ReferenceResolver(final ApiDescription local) {
054        this.local = Reject.checkNotNull(local);
055        map = new HashMap<>();
056        register(local);
057    }
058
059    /**
060     * Registers an external {@link ApiDescription}, for {@link org.forgerock.api.models.Reference} lookup, and
061     * must not have previously been registered.
062     *
063     * @param apiDescription {@link ApiDescription} to register, which has not previously been registered
064     * @return self
065     */
066    public ReferenceResolver register(final ApiDescription apiDescription) {
067        if (map.containsKey(apiDescription.getId())) {
068            throw new IllegalStateException("Already registered ID = " + apiDescription.getId());
069        }
070        map.put(apiDescription.getId(), apiDescription);
071        return this;
072    }
073
074    /**
075     * Registers external {@link ApiDescription}s, for {@link org.forgerock.api.models.Reference} lookup, and each
076     * must not have previously been registered.
077     *
078     * @param apiDescriptions List of {@link ApiDescription}s to register, which have not previously been registered
079     * @return self
080     */
081    public ReferenceResolver registerAll(final ApiDescription... apiDescriptions) {
082        for (final ApiDescription item : apiDescriptions) {
083            register(item);
084        }
085        return this;
086    }
087
088    /**
089     * Gets a {@link org.forgerock.api.models.Definitions} {@link Schema} by JSON reference.
090     *
091     * @param reference JSON reference
092     * @return {@link Schema} or {@code null} if not found
093     */
094    public Schema getDefinition(final Reference reference) {
095        return resolveDefinition(reference, new HashSet<String>());
096    }
097
098    private Schema resolveDefinition(final Reference reference, final Set<String> visitedRefs) {
099        final int nameStart = reference.getValue().indexOf(DEFINITIONS_REF);
100        if (nameStart != -1) {
101            final String name = reference.getValue().substring(nameStart + DEFINITIONS_REF.length());
102            if (!name.isEmpty()) {
103                if (!visitedRefs.add(reference.getValue())) {
104                    throw new IllegalStateException("Reference loop detected: " + reference.getValue());
105                }
106                if (nameStart == 0) {
107                    // there is no namespace, so do a local lookup
108                    if (local.getDefinitions() != null) {
109                        final Schema schema = local.getDefinitions().get(name);
110                        if (schema != null && schema.getReference() != null) {
111                            // reference chain
112                            return resolveDefinition(schema.getReference(), visitedRefs);
113                        }
114                        return schema;
115                    }
116                } else {
117                    final String namespace = reference.getValue().substring(0, nameStart);
118                    final ApiDescription apiDescription = map.get(namespace);
119                    if (apiDescription != null && apiDescription.getDefinitions() != null) {
120                        final Schema schema = apiDescription.getDefinitions().get(name);
121                        if (schema != null && schema.getReference() != null) {
122                            // reference chain
123                            return resolveDefinition(schema.getReference(), visitedRefs);
124                        }
125                        return schema;
126                    }
127                }
128            }
129        }
130        return null;
131    }
132
133    /**
134     * Gets and {@link org.forgerock.api.models.Errors} {@link ApiError} by JSON reference.
135     *
136     * @param reference JSON reference
137     * @return {@link ApiError} or {@code null} if not found
138     */
139    public ApiError getError(final Reference reference) {
140        return resolveError(reference, new HashSet<String>());
141    }
142
143    private ApiError resolveError(final Reference reference, final Set<String> visitedRefs) {
144        final int nameStart = reference.getValue().indexOf(ERRORS_REF);
145        if (nameStart != -1) {
146            final String name = reference.getValue().substring(nameStart + ERRORS_REF.length());
147            if (!name.isEmpty()) {
148                if (!visitedRefs.add(reference.getValue())) {
149                    throw new IllegalStateException("Reference loop detected: " + reference.getValue());
150                }
151                if (nameStart == 0) {
152                    // there is no namespace, so do a local lookup
153                    if (local.getErrors() != null) {
154                        final ApiError error = local.getErrors().get(name);
155                        if (error != null && error.getReference() != null) {
156                            // reference chain
157                            return resolveError(error.getReference(), visitedRefs);
158                        }
159                        return error;
160                    }
161                } else {
162                    final String namespace = reference.getValue().substring(0, nameStart);
163                    final ApiDescription apiDescription = map.get(namespace);
164                    if (apiDescription != null && apiDescription.getErrors() != null) {
165                        final ApiError error = apiDescription.getErrors().get(name);
166                        if (error != null && error.getReference() != null) {
167                            // reference chain
168                            return resolveError(error.getReference(), visitedRefs);
169                        }
170                        return error;
171                    }
172                }
173            }
174        }
175        return null;
176    }
177
178    /**
179     * Get a {@link org.forgerock.api.models.Services} {@link Resource} by JSON reference.
180     *
181     * @param reference JSON reference
182     * @return {@link Resource} or {@code null} if not found
183     */
184    public Resource getService(final Reference reference) {
185        return resolveService(reference, new HashSet<String>());
186    }
187
188    private Resource resolveService(final Reference reference, final Set<String> visitedRefs) {
189        final int nameStart = reference.getValue().indexOf(SERVICES_REF);
190        if (nameStart != -1) {
191            final String name = reference.getValue().substring(nameStart + SERVICES_REF.length());
192            if (!name.isEmpty()) {
193                if (!visitedRefs.add(reference.getValue())) {
194                    throw new IllegalStateException("Reference loop detected: " + reference.getValue());
195                }
196                if (nameStart == 0) {
197                    // there is no namespace, so do a local lookup
198                    if (local.getServices() != null) {
199                        final Resource service = local.getServices().get(name);
200                        if (service != null && service.getReference() != null) {
201                            // reference chain
202                            return resolveService(service.getReference(), visitedRefs);
203                        }
204                        return service;
205                    }
206                } else {
207                    final String namespace = reference.getValue().substring(0, nameStart);
208                    final ApiDescription apiDescription = map.get(namespace);
209                    if (apiDescription != null && apiDescription.getServices() != null) {
210                        final Resource service = apiDescription.getServices().get(name);
211                        if (service != null && service.getReference() != null) {
212                            // reference chain
213                            return resolveService(service.getReference(), visitedRefs);
214                        }
215                        return service;
216                    }
217                }
218            }
219        }
220        return null;
221    }
222}