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                    consist.add(engineAddress, engine.path(FORWARD).asBoolean());
111                }
112                consist.setPosition(engineAddress, engine.path(POSITION).asInt());
113                engines.add(engineAddress);
114            }
115            // remove engines if needed
116            ArrayList<DccLocoAddress> consistEngines = new ArrayList<>(consist.getConsistList());
117            consistEngines.stream()
118                    .filter(engineAddress -> (!engines.contains(engineAddress)))
119                    .forEach(consist::remove);
120        }
121        try {
122            (new ConsistFile()).writeFile(this.manager.getConsistList());
123        } catch (IOException ex) {
124            throw new JsonException(500, ex.getLocalizedMessage(), request.id);
125        }
126        return this.getConsist(address, request);
127    }
128
129    @Override
130    public JsonNode doPut(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
131        if (!this.manager.isConsistManager()) {
132            throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER),
133                    request.id); // NOI18N
134        }
135        LocoAddress address;
136        if (data.path(ADDRESS).canConvertToInt()) {
137            address = new DccLocoAddress(data.path(ADDRESS).asInt(), data.path(IS_LONG_ADDRESS).asBoolean(false));
138        } else {
139            address = JsonUtilHttpService.addressForString(data.path(ADDRESS).asText());
140        }
141        this.manager.getConsist(address);
142        return this.doPost(type, name, data, request);
143    }
144
145    @Override
146    public void doDelete(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
147        if (!this.manager.isConsistManager()) {
148            throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER),
149                    request.id); // NOI18N
150        }
151        if (!this.manager.getConsistList().contains(JsonUtilHttpService.addressForString(name))) {
152            throw new JsonException(404, Bundle.getMessage(request.locale, JsonException.ERROR_OBJECT, CONSIST, name),
153                    request.id); // NOI18N
154        }
155        this.manager.delConsist(JsonUtilHttpService.addressForString(name));
156    }
157
158    @Override
159    public JsonNode doGetList(String type, JsonNode data, JsonRequest request) throws JsonException {
160        if (!this.manager.isConsistManager()) {
161            throw new JsonException(503, Bundle.getMessage(request.locale, JsonConsist.ERROR_NO_CONSIST_MANAGER),
162                    request.id); // NOI18N
163        }
164        ArrayNode array = mapper.createArrayNode();
165        for (LocoAddress address : this.manager.getConsistList()) {
166            array.add(getConsist(address, request));
167        }
168        return message(array, request.id);
169    }
170
171    /**
172     * Get the JSON representation of a consist. The JSON representation is an
173     * object with the following data attributes:
174     * <ul>
175     * <li>address - integer address</li>
176     * <li>isLongAddress - boolean true if address is long, false if short</li>
177     * <li>type - integer, see {@link jmri.Consist#getConsistType() }</li>
178     * <li>id - string with consist Id</li>
179     * <li>sizeLimit - the maximum number of locomotives the consist can
180     * contain</li>
181     * <li>engines - array listing every locomotive in the consist. Each entry
182     * in the array contains the following attributes:
183     * <ul>
184     * <li>address - integer address</li>
185     * <li>isLongAddress - boolean true if address is long, false if short</li>
186     * <li>forward - boolean true if the locomotive running is forward in the
187     * consists</li>
188     * <li>position - integer locomotive's position in the consist</li>
189     * </ul>
190     * </ul>
191     *
192     * @param address The address of the consist to get
193     * @param request the JSON request
194     * @return The JSON representation of the consist
195     * @throws JsonException This exception has code 404 if the consist does not
196     *                       exist
197     */
198    public JsonNode getConsist(LocoAddress address, JsonRequest request) throws JsonException {
199        if (this.manager.getConsistList().contains(address)) {
200            ObjectNode data = mapper.createObjectNode();
201            Consist consist = this.manager.getConsist(address);
202            data.put(ADDRESS, consist.getConsistAddress().getNumber());
203            data.put(IS_LONG_ADDRESS, consist.getConsistAddress().isLongAddress());
204            data.put(TYPE, consist.getConsistType());
205            ArrayNode engines = data.putArray(ENGINES);
206            consist.getConsistList().stream().forEach(locomotive -> {
207                ObjectNode engine = mapper.createObjectNode();
208                engine.put(ADDRESS, locomotive.getNumber());
209                engine.put(IS_LONG_ADDRESS, locomotive.isLongAddress());
210                engine.put(FORWARD, consist.getLocoDirection(locomotive));
211                engine.put(POSITION, consist.getPosition(locomotive));
212                engines.add(engine);
213            });
214            data.put(NAME, consist.getConsistID());
215            data.put(SIZE_LIMIT, consist.sizeLimit());
216            return message(CONSIST, data, request.id);
217        } else {
218            throw new JsonException(404,
219                    Bundle.getMessage(request.locale, JsonException.ERROR_OBJECT, CONSIST, address.toString()),
220                    request.id); // NOI18N
221        }
222    }
223
224    @Override
225    public JsonNode doSchema(String type, boolean server, JsonRequest request) throws JsonException {
226        switch (type) {
227            case CONSIST:
228            case CONSISTS:
229                return doSchema(type,
230                        server,
231                        "jmri/server/json/consist/consist-server.json",
232                        "jmri/server/json/consist/consist-client.json",
233                        request.id);
234            default:
235                throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
236                        Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id);
237        }
238    }
239}