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