001package jmri.server.json.consist; 002 003import static jmri.server.json.JSON.ADDRESS; 004import static jmri.server.json.JSON.ENGINES; 005import static jmri.server.json.JSON.FORWARD; 006import static jmri.server.json.JSON.NAME; 007import static jmri.server.json.JSON.IS_LONG_ADDRESS; 008import static jmri.server.json.JSON.POSITION; 009import static jmri.server.json.JSON.SIZE_LIMIT; 010import static jmri.server.json.JSON.TYPE; 011import static jmri.server.json.consist.JsonConsist.CONSIST; 012import static jmri.server.json.consist.JsonConsist.CONSISTS; 013 014import com.fasterxml.jackson.databind.JsonNode; 015import com.fasterxml.jackson.databind.ObjectMapper; 016import com.fasterxml.jackson.databind.node.ArrayNode; 017import com.fasterxml.jackson.databind.node.ObjectNode; 018import java.io.IOException; 019import java.util.ArrayList; 020import javax.servlet.http.HttpServletResponse; 021import jmri.Consist; 022import jmri.DccLocoAddress; 023import jmri.InstanceManager; 024import jmri.LocoAddress; 025import jmri.jmrit.consisttool.ConsistFile; 026import jmri.server.json.JsonException; 027import jmri.server.json.JsonHttpService; 028import jmri.server.json.JsonRequest; 029import jmri.server.json.util.JsonUtilHttpService; 030 031/** 032 * @author Randall Wood Copyright 2016, 2018 033 */ 034public class JsonConsistHttpService extends JsonHttpService { 035 036 final JsonConsistManager manager; // default package visibility 037 038 public JsonConsistHttpService(ObjectMapper mapper) { 039 super(mapper); 040 this.manager = InstanceManager.getOptionalDefault(JsonConsistManager.class) 041 .orElseGet(() -> InstanceManager.setDefault(JsonConsistManager.class, new JsonConsistManager())); 042 } 043 044 @Override 045 public JsonNode doGet(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 046 if (!manager.isConsistManager()) { 047 throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER), 048 request.id); 049 } 050 return this.getConsist(JsonUtilHttpService.addressForString(name), request); 051 } 052 053 /** 054 * Change the properties and locomotives of a consist. This method takes as 055 * input the JSON representation of a consist as provided by 056 * {@link #getConsist }. If present in the 057 * JSON, this method sets the following consist properties: 058 * <ul> 059 * <li>consistID</li> 060 * <li>consistType</li> 061 * <li>locomotives (<em>engines</em> in the JSON representation)<br> 062 * <strong>NOTE</strong> Since this method adds, repositions, and deletes 063 * locomotives, the JSON representation must contain <em>every</em> 064 * locomotive that should be in the consist, if it contains the engines 065 * node.</li> 066 * </ul> 067 * 068 * @param type the JSON message type 069 * @param name the consist address, ignored if data contains an 070 * {@value jmri.server.json.JSON#ADDRESS} and 071 * {@value jmri.server.json.JSON#IS_LONG_ADDRESS} nodes 072 * @param data the consist as a JsonObject 073 * @param request the JSON request 074 * @return the JSON representation of the Consist 075 * @throws jmri.server.json.JsonException if there is no consist manager 076 * (code 503), the consist does not 077 * exist (code 404), or the consist 078 * cannot be saved (code 500). 079 */ 080 @Override 081 public JsonNode doPost(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 082 if (!this.manager.isConsistManager()) { 083 throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER), 084 request.id); // NOI18N 085 } 086 LocoAddress address; 087 if (data.path(ADDRESS).canConvertToInt()) { 088 address = new DccLocoAddress(data.path(ADDRESS).asInt(), data.path(IS_LONG_ADDRESS).asBoolean(false)); 089 } else { 090 address = JsonUtilHttpService.addressForString(data.path(ADDRESS).asText()); 091 } 092 if (!this.manager.getConsistList().contains(address)) { 093 throw new JsonException(404, Bundle.getMessage(request.locale, JsonException.ERROR_OBJECT, CONSIST, name), 094 request.id); 095 } 096 Consist consist = this.manager.getConsist(address); 097 if (data.path(NAME).isTextual()) { 098 consist.setConsistID(data.path(NAME).asText()); 099 } 100 if (data.path(TYPE).isInt()) { 101 consist.setConsistType(data.path(TYPE).asInt()); 102 } 103 if (data.path(ENGINES).isArray()) { 104 ArrayList<LocoAddress> engines = new ArrayList<>(); 105 // add every engine 106 for (JsonNode engine : data.path(ENGINES)) { 107 DccLocoAddress engineAddress = 108 new DccLocoAddress(engine.path(ADDRESS).asInt(), engine.path(IS_LONG_ADDRESS).asBoolean()); 109 if (!consist.contains(engineAddress)) { 110 removeFromOtherConsists(engineAddress, address); 111 consist.add(engineAddress, engine.path(FORWARD).asBoolean()); 112 } 113 consist.setPosition(engineAddress, engine.path(POSITION).asInt()); 114 engines.add(engineAddress); 115 } 116 // remove engines if needed 117 ArrayList<DccLocoAddress> consistEngines = new ArrayList<>(consist.getConsistList()); 118 consistEngines.stream() 119 .filter(engineAddress -> (!engines.contains(engineAddress))) 120 .forEach(consist::remove); 121 } 122 try { 123 (new ConsistFile()).writeFile(this.manager.getConsistList()); 124 } catch (IOException ex) { 125 throw new JsonException(500, ex.getLocalizedMessage(), request.id); 126 } 127 return this.getConsist(address, request); 128 } 129 130 /** 131 * Removes engineAddress from any OTHER consist that currently has it, 132 * so an operator who forgot to release an engine from an old consist 133 * can grab it into a new one instead of being blocked, while still 134 * guaranteeing an engine is never a member of two consists at once. 135 * 136 * @param engineAddress the engine about to be added 137 * @param excludeConsistAddress the consist it's being added to (skip 138 * this one, it's not "other") 139 */ 140 private void removeFromOtherConsists(DccLocoAddress engineAddress, LocoAddress excludeConsistAddress) { 141 for (LocoAddress otherConsistAddress : new ArrayList<>(this.manager.getConsistList())) { 142 if (otherConsistAddress.equals(excludeConsistAddress)) { 143 continue; 144 } 145 Consist other = this.manager.getConsist(otherConsistAddress); 146 if (other.contains(engineAddress)) { 147 other.remove(engineAddress); 148 } 149 } 150 } 151 152 @Override 153 public JsonNode doPut(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 154 if (!this.manager.isConsistManager()) { 155 throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER), 156 request.id); // NOI18N 157 } 158 LocoAddress address; 159 if (data.path(ADDRESS).canConvertToInt()) { 160 address = new DccLocoAddress(data.path(ADDRESS).asInt(), data.path(IS_LONG_ADDRESS).asBoolean(false)); 161 } else { 162 address = JsonUtilHttpService.addressForString(data.path(ADDRESS).asText()); 163 } 164 this.manager.getConsist(address); 165 return this.doPost(type, name, data, request); 166 } 167 168 @Override 169 public void doDelete(String type, String name, JsonNode data, JsonRequest request) throws JsonException { 170 if (!this.manager.isConsistManager()) { 171 throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER), 172 request.id); // NOI18N 173 } 174 if (!this.manager.getConsistList().contains(JsonUtilHttpService.addressForString(name))) { 175 throw new JsonException(404, Bundle.getMessage(request.locale, JsonException.ERROR_OBJECT, CONSIST, name), 176 request.id); // NOI18N 177 } 178 this.manager.delConsist(JsonUtilHttpService.addressForString(name)); 179 } 180 181 @Override 182 public JsonNode doGetList(String type, JsonNode data, JsonRequest request) throws JsonException { 183 if (!this.manager.isConsistManager()) { 184 throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER), 185 request.id); // NOI18N 186 } 187 ArrayNode array = mapper.createArrayNode(); 188 for (LocoAddress address : this.manager.getConsistList()) { 189 array.add(getConsist(address, request)); 190 } 191 return message(array, request.id); 192 } 193 194 /** 195 * Get the JSON representation of a consist. The JSON representation is an 196 * object with the following data attributes: 197 * <ul> 198 * <li>address - integer address</li> 199 * <li>isLongAddress - boolean true if address is long, false if short</li> 200 * <li>type - integer, see {@link jmri.Consist#getConsistType() }</li> 201 * <li>id - string with consist Id</li> 202 * <li>sizeLimit - the maximum number of locomotives the consist can 203 * contain</li> 204 * <li>engines - array listing every locomotive in the consist. Each entry 205 * in the array contains the following attributes: 206 * <ul> 207 * <li>address - integer address</li> 208 * <li>isLongAddress - boolean true if address is long, false if short</li> 209 * <li>forward - boolean true if the locomotive running is forward in the 210 * consists</li> 211 * <li>position - integer locomotive's position in the consist</li> 212 * </ul> 213 * </ul> 214 * 215 * @param address The address of the consist to get 216 * @param request the JSON request 217 * @return The JSON representation of the consist 218 * @throws JsonException This exception has code 404 if the consist does not 219 * exist 220 */ 221 public JsonNode getConsist(LocoAddress address, JsonRequest request) throws JsonException { 222 if (this.manager.getConsistList().contains(address)) { 223 ObjectNode data = mapper.createObjectNode(); 224 Consist consist = this.manager.getConsist(address); 225 data.put(ADDRESS, consist.getConsistAddress().getNumber()); 226 data.put(IS_LONG_ADDRESS, consist.getConsistAddress().isLongAddress()); 227 data.put(TYPE, consist.getConsistType()); 228 ArrayNode engines = data.putArray(ENGINES); 229 consist.getConsistList().stream().forEach(locomotive -> { 230 ObjectNode engine = mapper.createObjectNode(); 231 engine.put(ADDRESS, locomotive.getNumber()); 232 engine.put(IS_LONG_ADDRESS, locomotive.isLongAddress()); 233 engine.put(FORWARD, consist.getLocoDirection(locomotive)); 234 engine.put(POSITION, consist.getPosition(locomotive)); 235 engines.add(engine); 236 }); 237 data.put(NAME, consist.getConsistID()); 238 data.put(SIZE_LIMIT, consist.sizeLimit()); 239 return message(CONSIST, data, request.id); 240 } else { 241 throw new JsonException(404, 242 Bundle.getMessage(request.locale, JsonException.ERROR_OBJECT, CONSIST, address.toString()), 243 request.id); // NOI18N 244 } 245 } 246 247 @Override 248 public JsonNode doSchema(String type, boolean server, JsonRequest request) throws JsonException { 249 switch (type) { 250 case CONSIST: 251 case CONSISTS: 252 return doSchema(type, 253 server, 254 "jmri/server/json/consist/consist-server.json", 255 "jmri/server/json/consist/consist-client.json", 256 request.id); 257 default: 258 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 259 Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id); 260 } 261 } 262}