001package jmri.server.json;
002
003import com.fasterxml.jackson.databind.JsonNode;
004import com.fasterxml.jackson.databind.ObjectMapper;
005import com.fasterxml.jackson.databind.node.ArrayNode;
006import com.fasterxml.jackson.databind.node.ObjectNode;
007import java.util.Locale;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011import jmri.Manager;
012import jmri.NamedBean;
013
014/**
015 * Abstract implementation of JsonHttpService with specific support for
016 * {@link jmri.NamedBean} objects.
017 * <p>
018 * <strong>Note:</strong> if the extending class meets the requirements of
019 * {@link jmri.server.json.JsonNamedBeanHttpService}, it is recommended to
020 * extend that class instead.
021 *
022 * @author Randall Wood (C) 2016, 2019
023 * @param <T> the type supported by this service
024 */
025public abstract class JsonNonProvidedNamedBeanHttpService<T extends NamedBean> extends JsonHttpService {
026
027    public JsonNonProvidedNamedBeanHttpService(ObjectMapper mapper) {
028        super(mapper);
029    }
030
031    /**
032     * Respond to an HTTP GET request for a list of items of type.
033     * <p>
034     * This is called by the {@link jmri.web.servlet.json.JsonServlet} to handle
035     * get requests for a type, but no name. Services that do not have named
036     * objects, such as the {@link jmri.server.json.time.JsonTimeHttpService}
037     * should respond to this with a list containing a single JSON object.
038     * Services that can't return a list may throw a 400 Bad Request
039     * JsonException in this case.
040     *
041     * @param manager the manager for the requested type
042     * @param type    the type of the requested list
043     * @param data    JSON object possibly containing filters to limit the list
044     *                to
045     * @param request the JSON request
046     * @return a JSON list
047     * @throws JsonException may be thrown by concrete implementations
048     */
049    @Nonnull
050    protected final JsonNode doGetList(Manager<T> manager, String type, JsonNode data, JsonRequest request)
051            throws JsonException {
052        ArrayNode array = this.mapper.createArrayNode();
053        for (T bean : manager.getNamedBeanSet()) {
054            array.add(this.doGet(bean, bean.getSystemName(), type, request));
055        }
056        return message(array, request.id);
057    }
058
059    /**
060     * Respond to an HTTP GET request for a list of items of type.
061     * <p>
062     * This is called by the {@link jmri.web.servlet.json.JsonServlet} to handle
063     * get requests for a type, but no name. Services that do not have named
064     * objects, such as the {@link jmri.server.json.time.JsonTimeHttpService}
065     * should respond to this with a list containing a single JSON object.
066     * Services that can't return a list may throw a 400 Bad Request
067     * JsonException in this case.
068     *
069     * @param manager the manager for the requested type
070     * @param type    the type of the requested list
071     * @param data    JSON object possibly containing filters to limit the list
072     *                to
073     * @param locale  the requesting client's Locale
074     * @param id      the message id set by the client
075     * @return a JSON list
076     * @throws JsonException may be thrown by concrete implementations
077     */
078    @Nonnull
079    protected final JsonNode doGetList(Manager<T> manager, String type, JsonNode data, Locale locale, int id)
080            throws JsonException {
081        return doGetList(manager, type, data, new JsonRequest(locale, JSON.V5, JSON.GET, id));
082    }
083
084    /**
085     * Respond to an HTTP GET request for the requested name.
086     * <p>
087     * If name is null, return a list of all objects for the given type, if
088     * appropriate.
089     * <p>
090     * This method should throw a 500 Internal Server Error if type is not
091     * recognized.
092     *
093     * @param bean    the requested object
094     * @param name    the name of the requested object
095     * @param type    the type of the requested object
096     * @param request the JSON request
097     * @return a JSON description of the requested object
098     * @throws JsonException if the named object does not exist or other error
099     *                       occurs
100     */
101    @Nonnull
102    protected abstract ObjectNode doGet(T bean, @Nonnull String name, @Nonnull String type,
103            @Nonnull JsonRequest request)
104            throws JsonException;
105
106    /**
107     * Get the NamedBean matching name and type. If the request has a method
108     * other than GET, this may modify or create the NamedBean requested. Note
109     * that name or data may be null, but it is an error to have both be null.
110     *
111     * @param name    the name of the requested object
112     * @param type    the type of the requested object
113     * @param data    the JsonNode containing the JSON representation of the
114     *                bean to get
115     * @param request the JSON request
116     * @return the matching NamedBean or null if there is no match
117     * @throws JsonException            if the name is invalid for the type
118     * @throws IllegalArgumentException if both name is null and data is empty
119     */
120    @CheckForNull
121    protected abstract T getNamedBean(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data,
122            @Nonnull JsonRequest request) throws JsonException;
123
124    /**
125     * Create the JsonNode for a {@link jmri.NamedBean} object.
126     *
127     * @param bean    the bean to create the node for
128     * @param name    the name of the bean; used only if the bean is null
129     * @param type    the JSON type of the bean
130     * @param request the JSON request
131     * @return a JSON node
132     * @throws JsonException if the bean is null
133     */
134    @Nonnull
135    public ObjectNode getNamedBean(T bean, @Nonnull String name, @Nonnull String type, @Nonnull JsonRequest request)
136            throws JsonException {
137        if (bean == null) {
138            throw new JsonException(404, Bundle.getMessage(request.locale, JsonException.ERROR_NOT_FOUND, type, name),
139                    request.id);
140        }
141        ObjectNode data = mapper.createObjectNode();
142        data.put(JSON.NAME, bean.getSystemName());
143        data.put(JSON.USERNAME, bean.getUserName());
144        data.put(JSON.COMMENT, bean.getComment());
145        ArrayNode properties = data.putArray(JSON.PROPERTIES);
146        bean.getPropertyKeys().stream().forEach(key -> {
147            Object value = bean.getProperty(key);
148            if (value != null) {
149                properties.add(mapper.createObjectNode().put(key, value.toString()));
150            } else {
151                properties.add(mapper.createObjectNode().putNull(key));
152            }
153        });
154        return message(type, data, request.id);
155    }
156
157    /**
158     * Handle the common elements of a NamedBean that can be changed in an POST
159     * message.
160     * <p>
161     * <strong>Note:</strong> the system name of a NamedBean cannot be changed
162     * using this method.
163     *
164     * @param bean    the bean to modify
165     * @param data    the JsonNode containing the JSON representation of bean
166     * @param name    the system name of the bean
167     * @param type    the JSON type of the bean
168     * @param request the JSON request
169     * @return the bean so that this can be used in a method chain
170     * @throws JsonException if the bean is null
171     */
172    @Nonnull
173    protected T postNamedBean(T bean, @Nonnull JsonNode data, @Nonnull String name, @Nonnull String type,
174            @Nonnull JsonRequest request) throws JsonException {
175        if (bean == null) {
176            throw new JsonException(404, Bundle.getMessage(request.locale, JsonException.ERROR_NOT_FOUND, type, name),
177                    request.id);
178        }
179        if (data.path(JSON.USERNAME).isTextual()) {
180            bean.setUserName(data.path(JSON.USERNAME).asText());
181        }
182        if (!data.path(JSON.COMMENT).isMissingNode()) {
183            JsonNode comment = data.path(JSON.COMMENT);
184            bean.setComment(comment.isTextual() ? comment.asText() : null);
185        }
186        return bean;
187    }
188
189}