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, 073 Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 074 } 075 } 076 077 @Override 078 public JsonNode doPost(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 079 switch (type) { 080 case JsonRoster.ROSTER: 081 break; 082 case JsonRoster.ROSTER_ENTRY: 083 return postRosterEntry(request.locale, name, data, request.id); 084 case JsonRoster.ROSTER_GROUP: 085 break; 086 case JsonRoster.ROSTER_GROUPS: 087 break; 088 default: 089 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 090 Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 091 } 092 throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, 093 Bundle.getMessage(request.locale, "PostNotAllowed", type), request.id); 094 } 095 096 @Override 097 public JsonNode doGetList(String type, JsonNode data, JsonRequest request) throws JsonException { 098 switch (type) { 099 case JsonRoster.ROSTER: 100 case JsonRoster.ROSTER_ENTRY: 101 return getRoster(request.locale, mapper.createObjectNode(), request.id); 102 case JsonRoster.ROSTER_GROUP: 103 case JsonRoster.ROSTER_GROUPS: 104 return getRosterGroups(request); 105 default: 106 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 107 Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 108 } 109 } 110 111 @Nonnull 112 public JsonNode getRoster(@Nonnull Locale locale, @Nonnull JsonNode data, int id) throws JsonException { 113 String group = (!data.path(GROUP).isMissingNode()) ? data.path(GROUP).asText() : null; 114 if (Roster.ALLENTRIES.equals(group) || Roster.allEntries(locale).equals(group)) { 115 group = null; 116 } 117 String roadName = (!data.path(ROAD).isMissingNode()) ? data.path(ROAD).asText() : null; 118 String roadNumber = (!data.path(NUMBER).isMissingNode()) ? data.path(NUMBER).asText() : null; 119 String dccAddress = (!data.path(ADDRESS).isMissingNode()) ? data.path(ADDRESS).asText() : null; 120 String mfg = (!data.path(MFG).isMissingNode()) ? data.path(MFG).asText() : null; 121 String decoderModel = (!data.path(DECODER_MODEL).isMissingNode()) ? data.path(DECODER_MODEL).asText() : null; 122 String decoderFamily = (!data.path(DECODER_FAMILY).isMissingNode()) ? data.path(DECODER_FAMILY).asText() : null; 123 String name = (!data.path(NAME).isMissingNode()) ? data.path(NAME).asText() : null; 124 ArrayNode array = mapper.createArrayNode(); 125 var profile = Roster.getDefault(); 126 if ( profile != null ) { 127 for (RosterEntry entry : profile.getEntriesMatchingCriteria( 128 roadName, roadNumber, dccAddress, mfg, decoderModel, decoderFamily, name, group)) { 129 array.add(getRosterEntry(locale, entry, id)); 130 } 131 } 132 return message(array, id); 133 } 134 135 /** 136 * Returns the JSON representation of a roster entry. 137 * <p> 138 * Note that this returns, for images and icons, a URL relative to the root 139 * folder of the JMRI server. It is expected that clients will fill in the 140 * server IP address and port as they know it to be. 141 * 142 * @param locale the client's locale 143 * @param name the id of an entry in the roster 144 * @param id the message id set by the client 145 * @return a roster entry in JSON notation 146 * @throws jmri.server.json.JsonException If no roster entry exists for the 147 * given id 148 */ 149 public JsonNode getRosterEntry(Locale locale, String name, int id) throws JsonException { 150 var profile = Roster.getDefault(); 151 if ( profile != null ) { 152 var entry = profile.getEntryForId(name); 153 if ( entry != null ) { 154 return getRosterEntry(locale, entry, id); 155 } 156 } 157 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, 158 Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_ENTRY, name), id); 159 } 160 161 /** 162 * Returns the JSON representation of a roster entry. 163 * <p> 164 * Note that this returns, for images and icons, a URL relative to the root 165 * folder of the JMRI server. It is expected that clients will fill in the 166 * server IP address and port as they know it to be. 167 * 168 * @param locale the client's Locale 169 * @param entry A RosterEntry that may or may not be in the roster. 170 * @param id message id set by client 171 * @return a roster entry in JSON notation 172 * @throws jmri.server.json.JsonException if an error needs to be reported 173 * to the user 174 */ 175 public JsonNode getRosterEntry(Locale locale, @Nonnull RosterEntry entry, int id) throws JsonException { 176 String entryPath; 177 try { 178 entryPath = String.format("/%s/%s/", JsonRoster.ROSTER, 179 URLEncoder.encode(entry.getId(), StandardCharsets.UTF_8.toString())); 180 } catch (UnsupportedEncodingException ex) { 181 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 182 Bundle.getMessage(locale, "ErrorUnencodeable", JsonRoster.ROSTER_ENTRY, entry.getId(), NAME), id); 183 } 184 ObjectNode data = mapper.createObjectNode(); 185 data.put(NAME, entry.getId()); 186 data.put(ADDRESS, entry.getDccAddress()); 187 data.put(IS_LONG_ADDRESS, entry.isLongAddress()); 188 data.put(ROAD, entry.getRoadName()); 189 data.put(NUMBER, entry.getRoadNumber()); 190 data.put(MFG, entry.getMfg()); 191 data.put(DECODER_MODEL, entry.getDecoderModel()); 192 data.put(DECODER_FAMILY, entry.getDecoderFamily()); 193 data.put(MODEL, entry.getModel()); 194 data.put(COMMENT, entry.getComment()); 195 data.put(MAX_SPD_PCT, entry.getMaxSpeedPCT()); 196 data.put(IMAGE, (entry.getImagePath() != null) 197 ? entryPath + IMAGE 198 : null); 199 data.put(ICON, (entry.getIconPath() != null) 200 ? entryPath + ICON 201 : null); 202 data.put(SHUNTING_FUNCTION, entry.getShuntingFunction()); 203 data.put(OWNER, entry.getOwner()); 204 data.put(JsonRoster.DATE_MODIFIED, (entry.getDateModified() != null) 205 ? new StdDateFormat().format(entry.getDateModified()) 206 : null); 207 ArrayNode labels = data.putArray(FUNCTION_KEYS); 208 for (int i = 0; i <= entry.getMaxFnNumAsInt(); i++) { 209 ObjectNode label = mapper.createObjectNode(); 210 label.put(NAME, F + i); 211 label.put(LABEL, entry.getFunctionLabel(i)); 212 label.put(LOCKABLE, entry.getFunctionLockable(i)); 213 label.put(ICON, (entry.getFunctionImage(i) != null) 214 ? entryPath + F + i + "/" + ICON 215 : null); 216 label.put(SELECTED_ICON, (entry.getFunctionSelectedImage(i) != null) 217 ? entryPath + F + i + "/" + SELECTED_ICON 218 : null); 219 labels.add(label); 220 } 221 ArrayNode attributes = data.putArray(JsonRoster.ATTRIBUTES); 222 entry.getAttributes().stream().forEach(name -> { 223 ObjectNode attribute = mapper.createObjectNode(); 224 attribute.put(NAME, name); 225 attribute.put(VALUE, entry.getAttribute(name)); 226 attributes.add(attribute); 227 }); 228 ArrayNode rga = data.putArray(JsonRoster.ROSTER_GROUPS); 229 entry.getGroups().stream().forEach(group -> rga.add(group.getName())); 230 return message(JsonRoster.ROSTER_ENTRY, data, id); 231 } 232 233 /** 234 * Get a list of roster groups. 235 * 236 * @param request the JSON request 237 * @return a message containing the roster groups 238 * @throws JsonException if a requested roster group does not exist 239 */ 240 @Nonnull 241 public JsonNode getRosterGroups(JsonRequest request) throws JsonException { 242 ArrayNode array = mapper.createArrayNode(); 243 array.add(getRosterGroup(request.locale, Roster.ALLENTRIES, request.id)); 244 var profile = Roster.getDefault(); 245 if ( profile != null ) { 246 for (String name : profile.getRosterGroupList()) { 247 array.add(getRosterGroup(request.locale, name, request.id)); 248 } 249 } 250 return message(array, request.id); 251 } 252 253 @Nonnull 254 public JsonNode getRosterGroup(Locale locale, String name, int id) throws JsonException { 255 var profile = Roster.getDefault(); 256 if ( profile != null && 257 ( name.equals(Roster.ALLENTRIES) || profile.getRosterGroupList().contains(name))) { 258 int size = profile.getEntriesInGroup(name).size(); 259 ObjectNode data = mapper.createObjectNode(); 260 data.put(NAME, name.isEmpty() ? Roster.allEntries(locale) : name); 261 data.put(LENGTH, size); 262 return message(JsonRoster.ROSTER_GROUP, data, id); 263 } else { 264 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, 265 Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_GROUP, name), id); 266 } 267 } 268 269 @Override 270 public JsonNode doSchema(String type, boolean server, JsonRequest request) throws JsonException { 271 switch (type) { 272 case JsonRoster.ROSTER: 273 case JsonRoster.ROSTER_ENTRY: 274 return doSchema(type, 275 server, 276 "jmri/server/json/roster/" + type + "-server.json", 277 "jmri/server/json/roster/" + type + "-client.json", 278 request.id); 279 case JsonRoster.ROSTER_GROUP: 280 case JsonRoster.ROSTER_GROUPS: 281 return doSchema(type, 282 server, 283 "jmri/server/json/roster/rosterGroup-server.json", 284 "jmri/server/json/roster/rosterGroup-client.json", 285 request.id); 286 default: 287 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 288 Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 289 } 290 } 291 292 /** 293 * Edit an existing roster entry. 294 * 295 * @param locale the locale of the client 296 * @param name the roster entry id 297 * @param data the roster entry attributes to be edited 298 * @param id message id set by client 299 * @return the roster entry as edited 300 * @throws jmri.server.json.JsonException if an error needs to be reported 301 * to the user 302 */ 303 public JsonNode postRosterEntry(Locale locale, String name, JsonNode data, int id) throws JsonException { 304 305 RosterEntry re = null; 306 var defaultProfile = Roster.getDefault(); 307 if ( defaultProfile != null ) { // there may not be a default Profile set 308 re = defaultProfile.getEntryForId(name); 309 } 310 if ( re == null ) { 311 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, Bundle.getMessage 312 (locale, JsonException.ERROR_NOT_FOUND, JsonRoster.ROSTER_ENTRY, name), id); 313 } 314 final RosterEntry entry = re; 315 if (data.path(JsonRoster.ATTRIBUTES).isArray()) { 316 List<String> toKeep = new ArrayList<>(); 317 List<String> toRemove = new ArrayList<>(); 318 data.path(JsonRoster.ATTRIBUTES).forEach(attribute -> { 319 String key = attribute.path(NAME).asText(); 320 String value = attribute.path(VALUE).isNull() ? null : attribute.path(VALUE).asText(); 321 toKeep.add(key); 322 entry.putAttribute(key, value); 323 }); 324 entry.getAttributes() 325 .stream() 326 .filter(key -> (!toKeep.contains(key) && !key.startsWith(Roster.ROSTER_GROUP_PREFIX))) 327 .forEachOrdered(toRemove::add); 328 toRemove.forEach(entry::deleteAttribute); 329 } 330 if (data.path(JsonRoster.ROSTER_GROUPS).isArray()) { 331 List<String> toKeep = new ArrayList<>(); 332 List<String> toRemove = new ArrayList<>(); 333 data.path(JsonRoster.ROSTER_GROUPS).forEach(attribute -> { 334 String key = attribute.asText(); 335 String value = attribute.path(VALUE).isNull() ? null : attribute.path(VALUE).asText(); 336 toKeep.add(key); 337 entry.putAttribute(key, value); 338 }); 339 entry.getGroups() 340 .stream() 341 .filter(key -> (!toKeep.contains(Roster.ROSTER_GROUP_PREFIX + key))) 342 .forEachOrdered(key -> toRemove.add(Roster.ROSTER_GROUP_PREFIX + key)); 343 toRemove.forEach(entry::deleteAttribute); 344 } 345 if (data.path(FUNCTION_KEYS).isArray()) { 346 data.path(FUNCTION_KEYS).forEach(functionKey -> { 347 int function = Integer.parseInt(functionKey.path(NAME).asText().substring(F.length() - 1)); 348 entry.setFunctionLabel(function, 349 functionKey.path(LABEL).isNull() ? null : functionKey.path(LABEL).asText()); 350 entry.setFunctionLockable(function, functionKey.path(LOCKABLE).asBoolean()); 351 }); 352 } 353 if (data.path(ADDRESS).isTextual()) { 354 entry.setDccAddress(data.path(ADDRESS).asText()); 355 } 356 if (data.path(ROAD).isTextual()) { 357 entry.setRoadName(data.path(ROAD).asText()); 358 } 359 if (data.path(NUMBER).isTextual()) { 360 entry.setRoadNumber(data.path(NUMBER).asText()); 361 } 362 if (data.path(MFG).isTextual()) { 363 entry.setMfg(data.path(MFG).asText()); 364 } 365 if (data.path(MODEL).isTextual()) { 366 entry.setModel(data.path(MODEL).asText()); 367 } 368 if (!data.path(COMMENT).isMissingNode()) { 369 entry.setComment(data.path(COMMENT).isTextual() ? data.path(COMMENT).asText() : null); 370 } 371 if (data.path(MAX_SPD_PCT).isInt()) { 372 entry.setMaxSpeedPCT(data.path(MAX_SPD_PCT).asInt()); 373 } 374 if (!data.path(SHUNTING_FUNCTION).isMissingNode()) { 375 entry.setShuntingFunction( 376 data.path(SHUNTING_FUNCTION).isTextual() ? data.path(SHUNTING_FUNCTION).asText() : null); 377 } 378 if (!data.path(OWNER).isMissingNode()) { 379 entry.setOwner(data.path(OWNER).isTextual() ? data.path(OWNER).asText() : null); 380 } 381 entry.updateFile(); 382 return getRosterEntry(locale, entry, id); 383 } 384 385}