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;
007
008import java.util.List;
009import javax.annotation.Nonnull;
010import javax.annotation.CheckForNull;
011import javax.servlet.http.HttpServletResponse;
012import jmri.Manager;
013import jmri.NamedBean;
014import jmri.ProvidingManager;
015
016/**
017 * Abstract implementation of JsonHttpService with specific support for
018 * {@link jmri.NamedBean} objects.
019 * <p>
020 * <strong>Note:</strong> services requiring support for multiple classes of
021 * NamedBean cannot extend this class.
022 * <p>
023 * <strong>Note:</strong> NamedBeans must be managed by a
024 * {@link jmri.ProvidingManager} for this class to be used.
025 *
026 * @author Randall Wood (C) 2016, 2019
027 * @param <T> the type supported by this service
028 */
029public abstract class JsonNamedBeanHttpService<T extends NamedBean> extends JsonNonProvidedNamedBeanHttpService<T> {
030
031    public JsonNamedBeanHttpService(ObjectMapper mapper) {
032        super(mapper);
033    }
034
035    /**
036     * {@inheritDoc}
037     */
038    @Override
039    @Nonnull
040    public final JsonNode doGet(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data,
041            @Nonnull JsonRequest request) throws JsonException {
042        if (!type.equals(getType())) {
043            throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
044                    Bundle.getMessage(request.locale, JsonException.LOGGED_ERROR), request.id);
045        }
046        // NOTE: although allowing a user name to be used, a system name is recommended as it is
047        // less likely to suffer errors in translation between the allowed name and URL conversion
048        return doGet(this.getManager().getNamedBean(name), name, type, request);
049    }
050
051    /**
052     * {@inheritDoc}
053     */
054    @Override
055    @Nonnull
056    public final JsonNode doPost(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, @Nonnull JsonRequest request) throws JsonException {
057        if (!type.equals(getType())) {
058            throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
059                    Bundle.getMessage(request.locale, JsonException.LOGGED_ERROR), request.id);
060        }
061        // NOTE: although allowing a user name to be used, a system name is recommended as it is
062        // less likely to suffer errors in translation between the allowed name and URL conversion
063        T bean = postNamedBean(getManager().getNamedBean(name), data, name, type, request);
064        return doPost(bean, name, type, data, request);
065    }
066
067    /**
068     * {@inheritDoc}
069     *
070     * Override if the implementing class needs to prevent PUT methods from
071     * functioning or need to perform additional validation prior to creating
072     * the NamedBean.
073     */
074    @Override
075    public JsonNode doPut(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, @Nonnull JsonRequest request)
076            throws JsonException {
077        try {
078            getProvidingManager().provide(name);
079        } catch (IllegalArgumentException ex) {
080            throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
081                    Bundle.getMessage(request.locale, "ErrorInvalidSystemName", name, getType()), request.id);
082        } catch (Exception ex) {
083            throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
084                    Bundle.getMessage(request.locale, "ErrorCreatingObject", getType(), name), request.id);
085        }
086        return doPost(type, name, data, request);
087    }
088
089    /**
090     * {@inheritDoc}
091     */
092    @Nonnull
093    @Override
094    public final JsonNode doGetList(String type, JsonNode data, JsonRequest request) throws JsonException {
095        return doGetList(getManager(), type, data, request);
096    }
097
098    /**
099     * {@inheritDoc}
100     */
101    @Override
102    public void doDelete(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
103        if (!type.equals(getType())) {
104            throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
105                    Bundle.getMessage(request.locale, JsonException.LOGGED_ERROR), request.id);
106        }
107        // NOTE: although allowing a user name to be used, a system name is recommended as it is
108        // less likely to suffer errors in translation between the allowed name and URL conversion
109        doDelete(getManager().getNamedBean(name), name, type, data, request);
110    }
111
112    /**
113     * Respond to an HTTP GET request for the requested name.
114     * <p>
115     * If name is null, return a list of all objects for the given type, if
116     * appropriate.
117     * <p>
118     * This method should throw a 500 Internal Server Error if type is not
119     * recognized.
120     *
121     * @param bean   the requested object
122     * @param name   the name of the requested object
123     * @param type   the type of the requested object
124     * @param request the JSON request
125     * @return a JSON description of the requested object
126     * @throws JsonException if the named object does not exist or other error
127     *                           occurs
128     */
129    @Override
130    @Nonnull
131    protected abstract ObjectNode doGet(T bean, @Nonnull String name, @Nonnull String type, @Nonnull JsonRequest request)
132            throws JsonException;
133
134    /**
135     * {@inheritDoc}
136     */
137    @Override
138    @CheckForNull
139    public T getNamedBean(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, @Nonnull JsonRequest request) throws JsonException {
140        try {
141            if (!data.isEmpty() && !data.isNull()) {
142                if (JSON.PUT.equals(request.method)) {
143                    doPut(type, name, data, request);
144                } else if (JSON.POST.equals(request.method)) {
145                    doPost(type, name, data, request);
146                }
147            }
148            return getManager().getBySystemName(name);
149        } catch (IllegalArgumentException ex) {
150            throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, Bundle.getMessage(request.locale, "ErrorInvalidSystemName", name, type), request.id);
151        }
152    }
153
154    /**
155     * Respond to an HTTP POST request for the requested name.
156     *
157     * @param bean   the requested object
158     * @param name   the name of the requested object
159     * @param type   the type of the requested object
160     * @param data   data describing the requested object
161     * @param request the JSON request
162     * @return a JSON description of the requested object
163     * @throws JsonException if an error occurs
164     */
165    @Nonnull
166    protected abstract ObjectNode doPost(T bean, @Nonnull String name, @Nonnull String type, @Nonnull JsonNode data, @Nonnull JsonRequest request)
167            throws JsonException;
168
169    /**
170     * Delete the requested bean.
171     * <p>
172     * This method must be overridden to allow a bean to be deleted. The
173     * simplest overriding method body is:
174     * {@code deleteBean(bean, name, type, data, locale, id); }
175     *
176     * @param bean   the bean to delete
177     * @param name   the named of the bean to delete
178     * @param type   the type of the bean to delete
179     * @param data   data describing the named bean
180     * @param request the JSON request
181     * @throws JsonException if an error occurs
182     */
183    protected void doDelete(@CheckForNull T bean, @Nonnull String name, @Nonnull String type, @Nonnull JsonNode data, @Nonnull JsonRequest request) throws JsonException {
184        super.doDelete(type, name, data, request);
185    }
186
187    /**
188     * Delete the requested bean. This is the simplest method to delete a bean,
189     * and is likely to become the default implementation of
190     * {@link #doDelete} in an
191     * upcoming release of JMRI.
192     *
193     * @param bean   the bean to delete
194     * @param name   the named of the bean to delete
195     * @param type   the type of the bean to delete
196     * @param data   data describing the named bean
197     * @param request the JSON request
198     * @throws JsonException if an error occurs
199     */
200    protected final void deleteBean(@CheckForNull T bean, @Nonnull String name, @Nonnull String type, @Nonnull JsonNode data, @Nonnull JsonRequest request) throws JsonException {
201        if (bean == null) {
202            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
203                    Bundle.getMessage(request.locale, JsonException.ERROR_NOT_FOUND, type, name), request.id);
204        }
205        List<String> listeners = bean.getListenerRefs();
206        if (!listeners.isEmpty() && !acceptForceDeleteToken(type, name, data.path(JSON.FORCE_DELETE).asText())) {
207            ArrayNode conflicts = mapper.createArrayNode();
208            listeners.forEach(conflicts::add);
209            throwDeleteConflictException(type, name, conflicts, request);
210        } else {
211            getManager().deregister(bean);
212        }
213    }
214
215    /**
216     * Get the JSON type supported by this service.
217     *
218     * @return the JSON type
219     */
220    @Nonnull
221    protected abstract String getType();
222
223    /**
224     * Get the expected manager for the supported JSON type. This should
225     * normally be the default manager.
226     *
227     * @return the manager
228     */
229    @Nonnull
230    protected Manager<T> getManager() {
231        return getProvidingManager();
232    }
233
234    /**
235     * Get the expected providing manager for the supported JSON type. This
236     * should normally be the default manager.
237     *
238     * @return the providing manager
239     * @throws UnsupportedOperationException if a providing manager isn't available
240     */
241    protected abstract ProvidingManager<T> getProvidingManager()
242            throws UnsupportedOperationException;
243
244}