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.models;
018
019import static org.forgerock.api.enums.ParameterSource.*;
020import static org.forgerock.api.util.ValidationUtil.*;
021import static org.forgerock.util.Reject.*;
022
023import java.lang.reflect.Method;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.List;
027import java.util.Objects;
028import java.util.Set;
029import java.util.TreeSet;
030
031import com.fasterxml.jackson.annotation.JsonIgnore;
032import com.fasterxml.jackson.annotation.JsonInclude;
033import com.fasterxml.jackson.annotation.JsonProperty;
034import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
035import org.forgerock.api.ApiValidationException;
036import org.forgerock.api.annotations.Actions;
037import org.forgerock.api.annotations.CollectionProvider;
038import org.forgerock.util.i18n.LocalizableString;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * Class that represents the Items type in API descriptor.
044 */
045@JsonDeserialize(builder = Items.Builder.class)
046@JsonInclude(JsonInclude.Include.NON_NULL)
047public final class Items {
048
049    private static final Logger LOGGER = LoggerFactory.getLogger(Items.class);
050
051    private final Create create;
052    private final Read read;
053    private final Update update;
054    private final Delete delete;
055    private final Patch patch;
056    private final Action[] actions;
057    private final SubResources subresources;
058    private final Parameter pathParameter;
059
060    private Items(Builder builder) {
061        this.create = builder.create;
062        this.read = builder.read;
063        this.update = builder.update;
064        this.delete = builder.delete;
065        this.patch = builder.patch;
066        this.subresources = builder.subresources;
067        this.pathParameter = builder.pathParameter;
068        this.actions = builder.actions.toArray(new Action[builder.actions.size()]);
069
070        if (create == null && read == null && update == null && delete == null && patch == null && isEmpty(actions)) {
071            throw new ApiValidationException("At least one operation required");
072        }
073    }
074
075    /**
076     * Getter of Create.
077     *
078     * @return Create
079     */
080    public Create getCreate() {
081        return create;
082    }
083
084    /**
085     * Getter of Read.
086     *
087     * @return Read
088     */
089    public Read getRead() {
090        return read;
091    }
092
093    /**
094     * Getter of Update.
095     *
096     * @return Update
097     */
098    public Update getUpdate() {
099        return update;
100    }
101
102    /**
103     * Getter of Delete.
104     *
105     * @return Delete
106     */
107    public Delete getDelete() {
108        return delete;
109    }
110
111    /**
112     * Getter of Patch.
113     *
114     * @return Patch
115     */
116    public Patch getPatch() {
117        return patch;
118    }
119
120    /**
121     * Getter of actions.
122     *
123     * @return Actions
124     */
125    public Action[] getActions() {
126        return actions.length == 0 ? null : actions;
127    }
128
129    /**
130     * Getter of sub-resources.
131     *
132     * @return Sub-resources
133     */
134    public SubResources getSubresources() {
135        return subresources;
136    }
137
138    /**
139     * Get the path parameter.
140     *
141     * @return The path parameter.
142     */
143    public Parameter getPathParameter() {
144        return pathParameter;
145    }
146
147    @Override
148    public boolean equals(Object o) {
149        if (this == o) {
150            return true;
151        }
152        if (o == null || getClass() != o.getClass()) {
153            return false;
154        }
155        Items items = (Items) o;
156        return Objects.equals(create, items.create)
157                && Objects.equals(read, items.read)
158                && Objects.equals(update, items.update)
159                && Objects.equals(delete, items.delete)
160                && Objects.equals(patch, items.patch)
161                && Arrays.equals(actions, items.actions)
162                && Objects.equals(subresources, items.subresources)
163                && Objects.equals(pathParameter, items.pathParameter);
164    }
165
166    @Override
167    public int hashCode() {
168        return Objects.hash(create, read, update, delete, patch, actions, pathParameter, subresources);
169    }
170
171    /**
172     * Builds a {@link Resource} from this {@code Items} instance.
173     *
174     * @param mvccSupported {@code true} when MVCC is supported and {@code false} otherwise
175     * @param resourceSchema Resource-{@link Schema} or {@code null}
176     * @param title The resource title.
177     * @param description The resource description.
178     * @return New {@link Resource}
179     */
180    @JsonIgnore
181    public Resource asResource(boolean mvccSupported, Schema resourceSchema, LocalizableString title,
182            LocalizableString description) {
183        final List<Action> actions =
184                getActions() == null ? Collections.<Action>emptyList() : Arrays.asList(getActions());
185        return Resource.resource()
186                .mvccSupported(mvccSupported)
187                .resourceSchema(resourceSchema)
188                .title(title)
189                .description(description)
190                .create(getCreate())
191                .read(getRead())
192                .update(getUpdate())
193                .delete(getDelete())
194                .patch(getPatch())
195                .actions(actions)
196                .build();
197    }
198
199    /**
200     * Create a new Builder for Resoruce.
201     *
202     * @return Builder
203     */
204    public static Builder items() {
205        return new Builder();
206    }
207
208    /**
209     * Build an {@code Items} from an annotated request handler.
210     *
211     * @param type The annotated type.
212     * @param descriptor The root descriptor to add definitions to.
213     * @param subResources The sub resources.
214     * @return The built {@code Items} object.
215     */
216    public static Items fromAnnotatedType(Class<?> type, ApiDescription descriptor, SubResources subResources) {
217        final Builder builder = items();
218        final CollectionProvider provider = type.getAnnotation(CollectionProvider.class);
219        if (provider == null) {
220            LOGGER.info("Asked for Items for annotated type, but type does not have required RequestHandler"
221                    + " annotation. No api descriptor will be available for " + type);
222            return null;
223        }
224        builder.pathParameter(Parameter.fromAnnotation(type, provider.pathParam()));
225
226        for (final Method m : type.getMethods()) {
227            boolean instanceMethod = Arrays.asList(m.getParameterTypes()).indexOf(String.class) > -1;
228            org.forgerock.api.annotations.Action action = m.getAnnotation(org.forgerock.api.annotations.Action.class);
229            if (action != null && instanceMethod) {
230                builder.actions.add(Action.fromAnnotation(action, m, descriptor, type));
231            }
232            Actions actions = m.getAnnotation(Actions.class);
233            if (actions != null && instanceMethod) {
234                for (org.forgerock.api.annotations.Action a : actions.value()) {
235                    builder.actions.add(Action.fromAnnotation(a, null, descriptor, type));
236                }
237            }
238            org.forgerock.api.annotations.Create create = m.getAnnotation(org.forgerock.api.annotations.Create.class);
239            if (create != null) {
240                builder.create = Create.fromAnnotation(create, true, descriptor, type);
241            }
242            org.forgerock.api.annotations.Read read = m.getAnnotation(org.forgerock.api.annotations.Read.class);
243            if (read != null) {
244                builder.read = Read.fromAnnotation(read, descriptor, type);
245            }
246            org.forgerock.api.annotations.Update update =
247                    m.getAnnotation(org.forgerock.api.annotations.Update.class);
248            if (update != null) {
249                builder.update = Update.fromAnnotation(update, descriptor, type);
250            }
251            org.forgerock.api.annotations.Delete delete =
252                    m.getAnnotation(org.forgerock.api.annotations.Delete.class);
253            if (delete != null) {
254                builder.delete = Delete.fromAnnotation(delete, descriptor, type);
255            }
256            org.forgerock.api.annotations.Patch patch = m.getAnnotation(org.forgerock.api.annotations.Patch.class);
257            if (patch != null) {
258                builder.patch = Patch.fromAnnotation(patch, descriptor, type);
259            }
260        }
261
262        return builder.subresources(subResources).build();
263    }
264
265    /**
266     * Builder to help construct the {@code Items}.
267     */
268    public final static class Builder {
269        private Create create;
270        private Read read;
271        private Update update;
272        private Delete delete;
273        private Patch patch;
274        private SubResources subresources;
275        private Parameter pathParameter = Parameter.parameter().name("id").type("string").source(PATH).required(true)
276                .build();
277        private final Set<Action> actions;
278        private boolean built = false;
279
280        /**
281         * Private default constructor.
282         */
283        protected Builder() {
284            actions = new TreeSet<>();
285        }
286
287        /**
288         * Set create.
289         *
290         * @param create The create operation description, if supported
291         * @return Builder
292         */
293        @JsonProperty("create")
294        public Builder create(Create create) {
295            checkState();
296            this.create = create;
297            return this;
298        }
299
300        /**
301         * Set Read.
302         *
303         * @param read The read operation description, if supported
304         * @return Builder
305         */
306        @JsonProperty("read")
307        public Builder read(Read read) {
308            checkState();
309            this.read = read;
310            return this;
311        }
312
313        /**
314         * Set Update.
315         *
316         * @param update The update operation description, if supported
317         * @return Builder
318         */
319        @JsonProperty("update")
320        public Builder update(Update update) {
321            checkState();
322            this.update = update;
323            return this;
324        }
325
326        /**
327         * Set Delete.
328         *
329         * @param delete The delete operation description, if supported
330         * @return Builder
331         */
332        @JsonProperty("delete")
333        public Builder delete(Delete delete) {
334            checkState();
335            this.delete = delete;
336            return this;
337        }
338
339        /**
340         * Set Patch.
341         *
342         * @param patch The patch operation description, if supported
343         * @return Builder
344         */
345        @JsonProperty("patch")
346        public Builder patch(Patch patch) {
347            checkState();
348            this.patch = patch;
349            return this;
350        }
351
352        /**
353         * Set Actions.
354         *
355         * @param actions The list of action operation descriptions, if supported
356         * @return Builder
357         */
358        @JsonProperty("actions")
359        public Builder actions(List<Action> actions) {
360            checkState();
361            this.actions.addAll(actions);
362            return this;
363        }
364
365        /**
366         * Adds one Action to the list of Actions.
367         *
368         * @param action Action operation description to be added to the list
369         * @return Builder
370         */
371        public Builder action(Action action) {
372            checkState();
373            this.actions.add(action);
374            return this;
375        }
376
377        /**
378         * Sets the path parameter for this resource.
379         *
380         * @param pathParameter The path parameter definition.
381         * @return Builder
382         */
383        @JsonProperty("pathParameter")
384        public Builder pathParameter(Parameter pathParameter) {
385            checkState();
386            this.pathParameter = pathParameter;
387            return this;
388        }
389
390        /**
391         * Sets the sub-resources for this resource.
392         *
393         * @param subresources The sub-reosurces definition.
394         * @return Builder
395         */
396        @JsonProperty("subresources")
397        public Builder subresources(SubResources subresources) {
398            checkState();
399            this.subresources = subresources;
400            return this;
401        }
402
403        /**
404         * Construct a new instance of Resource.
405         *
406         * @return Resource instance
407         */
408        public Items build() {
409            checkState();
410            this.built = true;
411            if (create == null && read == null && update == null && delete == null && patch == null
412                    && actions.isEmpty() && subresources == null) {
413                return null;
414            }
415
416            return new Items(this);
417        }
418
419        private void checkState() {
420            rejectStateIfTrue(built, "Already built Items");
421        }
422
423    }
424}