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}