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