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}