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) { // there may not be a default Profile set 284 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, Bundle.getMessage 285 (locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_ENTRY, name), id); 286 } 287 if ( entry == null ) { 288 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, Bundle.getMessage 289 (locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_ENTRY, name), id); 290 } 291 if (data.path(JsonRoster.ATTRIBUTES).isArray()) { 292 List<String> toKeep = new ArrayList<>(); 293 List<String> toRemove = new ArrayList<>(); 294 data.path(JsonRoster.ATTRIBUTES).forEach(attribute -> { 295 String key = attribute.path(NAME).asText(); 296 String value = attribute.path(VALUE).isNull() ? null : attribute.path(VALUE).asText(); 297 toKeep.add(key); 298 entry.putAttribute(key, value); 299 }); 300 entry.getAttributes() 301 .stream() 302 .filter(key -> (!toKeep.contains(key) && !key.startsWith(Roster.ROSTER_GROUP_PREFIX))) 303 .forEachOrdered(toRemove::add); 304 toRemove.forEach(entry::deleteAttribute); 305 } 306 if (data.path(JsonRoster.ROSTER_GROUPS).isArray()) { 307 List<String> toKeep = new ArrayList<>(); 308 List<String> toRemove = new ArrayList<>(); 309 data.path(JsonRoster.ROSTER_GROUPS).forEach(attribute -> { 310 String key = attribute.asText(); 311 String value = attribute.path(VALUE).isNull() ? null : attribute.path(VALUE).asText(); 312 toKeep.add(key); 313 entry.putAttribute(key, value); 314 }); 315 entry.getGroups() 316 .stream() 317 .filter(key -> (!toKeep.contains(Roster.ROSTER_GROUP_PREFIX + key))) 318 .forEachOrdered(key -> toRemove.add(Roster.ROSTER_GROUP_PREFIX + key)); 319 toRemove.forEach(entry::deleteAttribute); 320 } 321 if (data.path(FUNCTION_KEYS).isArray()) { 322 data.path(FUNCTION_KEYS).forEach(functionKey -> { 323 int function = Integer.parseInt(functionKey.path(NAME).asText().substring(F.length() - 1)); 324 entry.setFunctionLabel(function, functionKey.path(LABEL).isNull() ? null : functionKey.path(LABEL).asText()); 325 entry.setFunctionLockable(function, functionKey.path(LOCKABLE).asBoolean()); 326 }); 327 } 328 if (data.path(ADDRESS).isTextual()) { 329 entry.setDccAddress(data.path(ADDRESS).asText()); 330 } 331 if (data.path(ROAD).isTextual()) { 332 entry.setRoadName(data.path(ROAD).asText()); 333 } 334 if (data.path(NUMBER).isTextual()) { 335 entry.setRoadNumber(data.path(NUMBER).asText()); 336 } 337 if (data.path(MFG).isTextual()) { 338 entry.setMfg(data.path(MFG).asText()); 339 } 340 if (data.path(MODEL).isTextual()) { 341 entry.setModel(data.path(MODEL).asText()); 342 } 343 if (!data.path(COMMENT).isMissingNode()) { 344 entry.setComment(data.path(COMMENT).isTextual() ? data.path(COMMENT).asText() : null); 345 } 346 if (data.path(MAX_SPD_PCT).isInt()) { 347 entry.setMaxSpeedPCT(data.path(MAX_SPD_PCT).asInt()); 348 } 349 if (!data.path(SHUNTING_FUNCTION).isMissingNode()) { 350 entry.setShuntingFunction(data.path(SHUNTING_FUNCTION).isTextual() ? data.path(SHUNTING_FUNCTION).asText() : null); 351 } 352 if (!data.path(OWNER).isMissingNode()) { 353 entry.setOwner(data.path(OWNER).isTextual() ? data.path(OWNER).asText() : null); 354 } 355 entry.updateFile(); 356 return getRosterEntry(locale, entry, id); 357 } 358 359}