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}