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}