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 */
016package org.forgerock.api.jackson;
017
018import static org.forgerock.api.jackson.JacksonUtils.OBJECT_MAPPER;
019
020import com.fasterxml.jackson.databind.JavaType;
021import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema;
022import com.fasterxml.jackson.module.jsonSchema.jakarta.types.ArraySchema;
023import jakarta.validation.ValidationException;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.List;
029import org.forgerock.api.enums.ReadPolicy;
030import org.forgerock.api.enums.WritePolicy;
031import org.forgerock.json.JsonValue;
032
033/**
034 * An extension to the Jackson {@code ArraySchema} that includes the custom CREST JSON Schema attributes.
035 */
036public class CrestArraySchema extends ArraySchema implements CrestReadWritePoliciesSchema, OrderedFieldSchema,
037        ValidatableSchema, WithExampleSchema<List<Object>> {
038    private static final JavaType EXAMPLE_VALUE_TYPE = OBJECT_MAPPER.getTypeFactory()
039            .constructParametrizedType(ArrayList.class, List.class, Object.class);
040
041    private WritePolicy writePolicy;
042    private ReadPolicy readPolicy;
043    private Boolean errorOnWritePolicyFailure;
044    private Boolean returnOnDemand;
045    private Integer propertyOrder;
046    private List<Object> example;
047
048    @Override
049    public WritePolicy getWritePolicy() {
050        return writePolicy;
051    }
052
053    @Override
054    public void setWritePolicy(WritePolicy policy) {
055        this.writePolicy = policy;
056    }
057
058    @Override
059    public ReadPolicy getReadPolicy() {
060        return readPolicy;
061    }
062
063    @Override
064    public void setReadPolicy(ReadPolicy readPolicy) {
065        this.readPolicy = readPolicy;
066    }
067
068    @Override
069    public Boolean getErrorOnWritePolicyFailure() {
070        return errorOnWritePolicyFailure;
071    }
072
073    @Override
074    public void setErrorOnWritePolicyFailure(Boolean errorOnWritePolicyFailure) {
075        this.errorOnWritePolicyFailure = errorOnWritePolicyFailure;
076    }
077
078    @Override
079    public Boolean getReturnOnDemand() {
080        return returnOnDemand;
081    }
082
083    @Override
084    public void setReturnOnDemand(Boolean returnOnDemand) {
085        this.returnOnDemand = returnOnDemand;
086    }
087
088    @Override
089    public Integer getPropertyOrder() {
090        return propertyOrder;
091    }
092
093    @Override
094    public void setPropertyOrder(Integer order) {
095        this.propertyOrder = order;
096    }
097
098    @Override
099    public void validate(JsonValue object) throws ValidationException {
100        if (!object.isCollection()) {
101            throw new ValidationException("Array expected, but got: " + object.getObject());
102        }
103        if (maxItems != null && object.size() > maxItems) {
104            throw new ValidationException("Array has too many items. Maximum permitted: " + maxItems);
105        }
106        if (minItems != null && object.size() < minItems) {
107            throw new ValidationException("Array has too few items. Minimum permitted: " + minItems);
108        }
109        if (items.isSingleItems()) {
110            if (items.asSingleItems().getSchema() instanceof ValidatableSchema) {
111                ValidatableSchema itemSchema = (ValidatableSchema) items.asSingleItems().getSchema();
112                for (JsonValue item : object) {
113                    itemSchema.validate(item);
114                }
115            }
116        } else {
117            Iterator<JsonValue> arrayItems = object.iterator();
118            for (JsonSchema itemSchema : items.asArrayItems().getJsonSchemas()) {
119                if (!arrayItems.hasNext()) {
120                    throw new ValidationException("Not enough items. Expecting " + itemSchema);
121                }
122                if (itemSchema instanceof ValidatableSchema) {
123                    ((ValidatableSchema) itemSchema).validate(arrayItems.next());
124                }
125            }
126        }
127    }
128
129    @Override
130    public List<Object> getExample() {
131        List<Object> example = this.example;
132        if (example == null) {
133            example = new ArrayList<>();
134            boolean foundExample = items.isSingleItems()
135                    ? applySingleSchemaExample(example)
136                    : applyMultipleSchemasExamples(example);
137            if (!foundExample) {
138                example = null;
139            }
140        }
141        return example;
142    }
143
144    private boolean applySingleSchemaExample(List<Object> example) {
145        boolean foundExample = false;
146        if (items.asSingleItems().getSchema() instanceof WithExampleSchema) {
147            Object itemsExample = ((WithExampleSchema) items.asSingleItems().getSchema()).getExample();
148            if (itemsExample != null) {
149                int count = minItems != null && minItems > 1 ? minItems : 1;
150                foundExample = true;
151                for (int i = 0; i < count; i++) {
152                    example.add(itemsExample);
153                }
154            }
155        }
156        return foundExample;
157    }
158
159    private boolean applyMultipleSchemasExamples(List<Object> example) {
160        boolean foundExample = false;
161        for (JsonSchema schema : items.asArrayItems().getJsonSchemas()) {
162            if (schema instanceof WithExampleSchema) {
163                Object propertyExample = ((WithExampleSchema) schema).getExample();
164                if (propertyExample != null) {
165                    foundExample = true;
166                    example.add(propertyExample);
167                } else {
168                    example.add(new HashMap<>());
169                }
170            }
171        }
172        return foundExample;
173    }
174
175    @Override
176    public void setExample(String example) throws IOException {
177        this.example = OBJECT_MAPPER.readValue(example, EXAMPLE_VALUE_TYPE);
178    }
179}