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