001package jmri.server.json.roster; 002 003import static jmri.server.json.JSON.ADDRESS; 004import static jmri.server.json.JSON.COMMENT; 005import static jmri.server.json.JSON.DECODER_FAMILY; 006import static jmri.server.json.JSON.DECODER_MODEL; 007import static jmri.server.json.JSON.F; 008import static jmri.server.json.JSON.FUNCTION_KEYS; 009import static jmri.server.json.JSON.GROUP; 010import static jmri.server.json.JSON.ICON; 011import static jmri.server.json.JSON.IMAGE; 012import static jmri.server.json.JSON.IS_LONG_ADDRESS; 013import static jmri.server.json.JSON.LABEL; 014import static jmri.server.json.JSON.LENGTH; 015import static jmri.server.json.JSON.LOCKABLE; 016import static jmri.server.json.JSON.MAX_SPD_PCT; 017import static jmri.server.json.JSON.MFG; 018import static jmri.server.json.JSON.MODEL; 019import static jmri.server.json.JSON.NAME; 020import static jmri.server.json.JSON.NUMBER; 021import static jmri.server.json.JSON.OWNER; 022import static jmri.server.json.JSON.ROAD; 023import static jmri.server.json.JSON.SELECTED_ICON; 024import static jmri.server.json.JSON.SHUNTING_FUNCTION; 025import static jmri.server.json.JSON.VALUE; 026 027import com.fasterxml.jackson.databind.JsonNode; 028import com.fasterxml.jackson.databind.ObjectMapper; 029import com.fasterxml.jackson.databind.node.ArrayNode; 030import com.fasterxml.jackson.databind.node.ObjectNode; 031import com.fasterxml.jackson.databind.util.StdDateFormat; 032import java.io.UnsupportedEncodingException; 033import java.net.URLEncoder; 034import java.nio.charset.StandardCharsets; 035import java.util.ArrayList; 036import java.util.List; 037import java.util.Locale; 038import javax.annotation.Nonnull; 039import javax.servlet.http.HttpServletResponse; 040import jmri.jmrit.roster.Roster; 041import jmri.jmrit.roster.RosterEntry; 042import jmri.server.json.JsonException; 043import jmri.server.json.JsonHttpService; 044import jmri.server.json.JsonRequest; 045 046/** 047 * 048 * @author Randall Wood Copyright 2016, 2018 049 */ 050public class JsonRosterHttpService extends JsonHttpService { 051 052 public JsonRosterHttpService(ObjectMapper mapper) { 053 super(mapper); 054 } 055 056 @Override 057 public JsonNode doGet(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 058 switch (type) { 059 case JsonRoster.ROSTER: 060 ObjectNode node = mapper.createObjectNode(); 061 if (!name.isEmpty()) { 062 node.put(GROUP, name); 063 } 064 return getRoster(request.locale, node, request.id); 065 case JsonRoster.ROSTER_ENTRY: 066 return getRosterEntry(request.locale, name, request.id); 067 case JsonRoster.ROSTER_GROUP: 068 return getRosterGroup(request.locale, name, request.id); 069 case JsonRoster.ROSTER_GROUPS: 070 return getRosterGroups(request); 071 default: 072 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 073 } 074 } 075 076 @Override 077 public JsonNode doPost(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 078 switch (type) { 079 case JsonRoster.ROSTER: 080 break; 081 case JsonRoster.ROSTER_ENTRY: 082 return postRosterEntry(request.locale, name, data, request.id); 083 case JsonRoster.ROSTER_GROUP: 084 break; 085 case JsonRoster.ROSTER_GROUPS: 086 break; 087 default: 088 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 089 } 090 throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, Bundle.getMessage(request.locale, "PostNotAllowed", type), request.id); 091 } 092 093 @Override 094 public JsonNode doGetList(String type, JsonNode data, JsonRequest request) throws JsonException { 095 switch (type) { 096 case JsonRoster.ROSTER: 097 case JsonRoster.ROSTER_ENTRY: 098 return getRoster(request.locale, mapper.createObjectNode(), request.id); 099 case JsonRoster.ROSTER_GROUP: 100 case JsonRoster.ROSTER_GROUPS: 101 return getRosterGroups(request); 102 default: 103 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 104 } 105 } 106 107 public JsonNode getRoster(@Nonnull Locale locale, @Nonnull JsonNode data, int id) throws JsonException { 108 String group = (!data.path(GROUP).isMissingNode()) ? data.path(GROUP).asText() : null; 109 if (Roster.ALLENTRIES.equals(group) || Roster.allEntries(locale).equals(group)) { 110 group = null; 111 } 112 String roadName = (!data.path(ROAD).isMissingNode()) ? data.path(ROAD).asText() : null; 113 String roadNumber = (!data.path(NUMBER).isMissingNode()) ? data.path(NUMBER).asText() : null; 114 String dccAddress = (!data.path(ADDRESS).isMissingNode()) ? data.path(ADDRESS).asText() : null; 115 String mfg = (!data.path(MFG).isMissingNode()) ? data.path(MFG).asText() : null; 116 String decoderModel = (!data.path(DECODER_MODEL).isMissingNode()) ? data.path(DECODER_MODEL).asText() : null; 117 String decoderFamily = (!data.path(DECODER_FAMILY).isMissingNode()) ? data.path(DECODER_FAMILY).asText() : null; 118 String name = (!data.path(NAME).isMissingNode()) ? data.path(NAME).asText() : null; 119 ArrayNode array = mapper.createArrayNode(); 120 for (RosterEntry entry : Roster.getDefault().getEntriesMatchingCriteria(roadName, roadNumber, dccAddress, mfg, decoderModel, decoderFamily, name, group)) { 121 array.add(getRosterEntry(locale, entry, id)); 122 } 123 return message(array, id); 124 } 125 126 /** 127 * Returns the JSON representation of a roster entry. 128 * <p> 129 * Note that this returns, for images and icons, a URL relative to the root 130 * folder of the JMRI server. It is expected that clients will fill in the 131 * server IP address and port as they know it to be. 132 * 133 * @param locale the client's locale 134 * @param name the id of an entry in the roster 135 * @param id the message id set by the client 136 * @return a roster entry in JSON notation 137 * @throws jmri.server.json.JsonException If no roster entry exists for the 138 * given id 139 */ 140 public JsonNode getRosterEntry(Locale locale, String name, int id) throws JsonException { 141 try { 142 return getRosterEntry(locale, Roster.getDefault().getEntryForId(name), id); 143 } catch (NullPointerException ex) { 144 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_ENTRY, name), id); 145 } 146 } 147 148 /** 149 * Returns the JSON representation of a roster entry. 150 * <p> 151 * Note that this returns, for images and icons, a URL relative to the root 152 * folder of the JMRI server. It is expected that clients will fill in the 153 * server IP address and port as they know it to be. 154 * 155 * @param locale the client's Locale 156 * @param entry A RosterEntry that may or may not be in the roster. 157 * @param id message id set by client 158 * @return a roster entry in JSON notation 159 * @throws jmri.server.json.JsonException if an error needs to be reported 160 * to the user 161 */ 162 public JsonNode getRosterEntry(Locale locale, @Nonnull RosterEntry entry, int id) throws JsonException { 163 String entryPath; 164 try { 165 entryPath = String.format("/%s/%s/", JsonRoster.ROSTER, URLEncoder.encode(entry.getId(), StandardCharsets.UTF_8.toString())); 166 } catch (UnsupportedEncodingException ex) { 167 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle.getMessage(locale, "ErrorUnencodeable", JsonRoster.ROSTER_ENTRY, entry.getId(), NAME), id); 168 } 169 ObjectNode data = mapper.createObjectNode(); 170 data.put(NAME, entry.getId()); 171 data.put(ADDRESS, entry.getDccAddress()); 172 data.put(IS_LONG_ADDRESS, entry.isLongAddress()); 173 data.put(ROAD, entry.getRoadName()); 174 data.put(NUMBER, entry.getRoadNumber()); 175 data.put(MFG, entry.getMfg()); 176 data.put(DECODER_MODEL, entry.getDecoderModel()); 177 data.put(DECODER_FAMILY, entry.getDecoderFamily()); 178 data.put(MODEL, entry.getModel()); 179 data.put(COMMENT, entry.getComment()); 180 data.put(MAX_SPD_PCT, entry.getMaxSpeedPCT()); 181 data.put(IMAGE, (entry.getImagePath() != null) 182 ? entryPath + IMAGE 183 : null); 184 data.put(ICON, (entry.getIconPath() != null) 185 ? entryPath + ICON 186 : null); 187 data.put(SHUNTING_FUNCTION, entry.getShuntingFunction()); 188 data.put(OWNER, entry.getOwner()); 189 data.put(JsonRoster.DATE_MODIFIED, (entry.getDateModified() != null) 190 ? new StdDateFormat().format(entry.getDateModified()) 191 : null); 192 ArrayNode labels = data.putArray(FUNCTION_KEYS); 193 for (int i = 0; i <= entry.getMaxFnNumAsInt(); i++) { 194 ObjectNode label = mapper.createObjectNode(); 195 label.put(NAME, F + i); 196 label.put(LABEL, entry.getFunctionLabel(i)); 197 label.put(LOCKABLE, entry.getFunctionLockable(i)); 198 label.put(ICON, (entry.getFunctionImage(i) != null) 199 ? entryPath + F + i + "/" + ICON 200 : null); 201 label.put(SELECTED_ICON, (entry.getFunctionSelectedImage(i) != null) 202 ? entryPath + F + i + "/" + SELECTED_ICON 203 : null); 204 labels.add(label); 205 } 206 ArrayNode attributes = data.putArray(JsonRoster.ATTRIBUTES); 207 entry.getAttributes().stream().forEach(name -> { 208 ObjectNode attribute = mapper.createObjectNode(); 209 attribute.put(NAME, name); 210 attribute.put(VALUE, entry.getAttribute(name)); 211 attributes.add(attribute); 212 }); 213 ArrayNode rga = data.putArray(JsonRoster.ROSTER_GROUPS); 214 entry.getGroups().stream().forEach(group -> rga.add(group.getName())); 215 return message(JsonRoster.ROSTER_ENTRY, data, id); 216 } 217 218 /** 219 * Get a list of roster groups. 220 * 221 * @param request the JSON request 222 * @return a message containing the roster groups 223 * @throws JsonException if a requested roster group does not exist 224 */ 225 public JsonNode getRosterGroups(JsonRequest request) throws JsonException { 226 ArrayNode array = mapper.createArrayNode(); 227 array.add(getRosterGroup(request.locale, Roster.ALLENTRIES, request.id)); 228 for (String name : Roster.getDefault().getRosterGroupList()) { 229 array.add(getRosterGroup(request.locale, name, request.id)); 230 } 231 return message(array, request.id); 232 } 233 234 public JsonNode getRosterGroup(Locale locale, String name, int id) throws JsonException { 235 if (name.equals(Roster.ALLENTRIES) || Roster.getDefault().getRosterGroupList().contains(name)) { 236 int size = Roster.getDefault().getEntriesInGroup(name).size(); 237 ObjectNode data = mapper.createObjectNode(); 238 data.put(NAME, name.isEmpty() ? Roster.allEntries(locale) : name); 239 data.put(LENGTH, size); 240 return message(JsonRoster.ROSTER_GROUP, data, id); 241 } else { 242 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_GROUP, name), id); 243 } 244 } 245 246 @Override 247 public JsonNode doSchema(String type, boolean server, JsonRequest request) throws JsonException { 248 switch (type) { 249 case JsonRoster.ROSTER: 250 case JsonRoster.ROSTER_ENTRY: 251 return doSchema(type, 252 server, 253 "jmri/server/json/roster/" + type + "-server.json", 254 "jmri/server/json/roster/" + type + "-client.json", 255 request.id); 256 case JsonRoster.ROSTER_GROUP: 257 case JsonRoster.ROSTER_GROUPS: 258 return doSchema(type, 259 server, 260 "jmri/server/json/roster/rosterGroup-server.json", 261 "jmri/server/json/roster/rosterGroup-client.json", 262 request.id); 263 default: 264 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 265 } 266 } 267 268 /** 269 * Edit an existing roster entry. 270 * 271 * @param locale the locale of the client 272 * @param name the roster entry id 273 * @param data the roster entry attributes to be edited 274 * @param id message id set by client 275 * @return the roster entry as edited 276 * @throws jmri.server.json.JsonException if an error needs to be reported 277 * to the user 278 */ 279 public JsonNode postRosterEntry(Locale locale, String name, JsonNode data, int id) throws JsonException { 280 RosterEntry entry; 281 try { 282 entry = Roster.getDefault().getEntryForId(name); 283 } catch (NullPointerException ex) { 284 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_ENTRY, name), id); 285 } 286 if (data.path(JsonRoster.ATTRIBUTES).isArray()) { 287 List<String> toKeep = new ArrayList<>(); 288 List<String> toRemove = new ArrayList<>(); 289 data.path(JsonRoster.ATTRIBUTES).forEach(attribute -> { 290 String key = attribute.path(NAME).asText(); 291 String value = attribute.path(VALUE).isNull() ? null : attribute.path(VALUE).asText(); 292 toKeep.add(key); 293 entry.putAttribute(key, value); 294 }); 295 entry.getAttributes() 296 .stream() 297 .filter(key -> (!toKeep.contains(key) && !key.startsWith(Roster.ROSTER_GROUP_PREFIX))) 298 .forEachOrdered(toRemove::add); 299 toRemove.forEach(entry::deleteAttribute); 300 } 301 if (data.path(JsonRoster.ROSTER_GROUPS).isArray()) { 302 List<String> toKeep = new ArrayList<>(); 303 List<String> toRemove = new ArrayList<>(); 304 data.path(JsonRoster.ROSTER_GROUPS).forEach(attribute -> { 305 String key = attribute.asText(); 306 String value = attribute.path(VALUE).isNull() ? null : attribute.path(VALUE).asText(); 307 toKeep.add(key); 308 entry.putAttribute(key, value); 309 }); 310 entry.getGroups() 311 .stream() 312 .filter(key -> (!toKeep.contains(Roster.ROSTER_GROUP_PREFIX + key))) 313 .forEachOrdered(key -> toRemove.add(Roster.ROSTER_GROUP_PREFIX + key)); 314 toRemove.forEach(entry::deleteAttribute); 315 } 316 if (data.path(FUNCTION_KEYS).isArray()) { 317 data.path(FUNCTION_KEYS).forEach(functionKey -> { 318 int function = Integer.parseInt(functionKey.path(NAME).asText().substring(F.length() - 1)); 319 entry.setFunctionLabel(function, functionKey.path(LABEL).isNull() ? null : functionKey.path(LABEL).asText()); 320 entry.setFunctionLockable(function, functionKey.path(LOCKABLE).asBoolean()); 321 }); 322 } 323 if (data.path(ADDRESS).isTextual()) { 324 entry.setDccAddress(data.path(ADDRESS).asText()); 325 } 326 if (data.path(ROAD).isTextual()) { 327 entry.setRoadName(data.path(ROAD).asText()); 328 } 329 if (data.path(NUMBER).isTextual()) { 330 entry.setRoadNumber(data.path(NUMBER).asText()); 331 } 332 if (data.path(MFG).isTextual()) { 333 entry.setMfg(data.path(MFG).asText()); 334 } 335 if (data.path(MODEL).isTextual()) { 336 entry.setModel(data.path(MODEL).asText()); 337 } 338 if (!data.path(COMMENT).isMissingNode()) { 339 entry.setComment(data.path(COMMENT).isTextual() ? data.path(COMMENT).asText() : null); 340 } 341 if (data.path(MAX_SPD_PCT).isInt()) { 342 entry.setMaxSpeedPCT(data.path(MAX_SPD_PCT).asInt()); 343 } 344 if (!data.path(SHUNTING_FUNCTION).isMissingNode()) { 345 entry.setShuntingFunction(data.path(SHUNTING_FUNCTION).isTextual() ? data.path(SHUNTING_FUNCTION).asText() : null); 346 } 347 if (!data.path(OWNER).isMissingNode()) { 348 entry.setOwner(data.path(OWNER).isTextual() ? data.path(OWNER).asText() : null); 349 } 350 entry.updateFile(); 351 return getRosterEntry(locale, entry, id); 352 } 353 354}