001package jmri.web.servlet.roster;
002
003import static jmri.server.json.JSON.ADDRESS;
004import static jmri.server.json.JSON.DATA;
005import static jmri.server.json.JSON.DECODER_FAMILY;
006import static jmri.server.json.JSON.DECODER_MODEL;
007import static jmri.server.json.JSON.FORMAT;
008import static jmri.server.json.JSON.GROUP;
009import static jmri.server.json.JSON.ID;
010import static jmri.server.json.JSON.LIST;
011import static jmri.server.json.JSON.MFG;
012import static jmri.server.json.JSON.NAME;
013import static jmri.server.json.JSON.NUMBER;
014import static jmri.server.json.JSON.ROAD;
015import static jmri.server.json.JSON.V5;
016import static jmri.web.servlet.ServletUtil.IMAGE_PNG;
017import static jmri.web.servlet.ServletUtil.UTF8;
018import static jmri.web.servlet.ServletUtil.UTF8_APPLICATION_JSON;
019import static jmri.web.servlet.ServletUtil.UTF8_APPLICATION_XML;
020import static jmri.web.servlet.ServletUtil.UTF8_TEXT_HTML;
021
022import com.fasterxml.jackson.databind.JsonNode;
023import com.fasterxml.jackson.databind.ObjectMapper;
024import com.fasterxml.jackson.databind.node.ObjectNode;
025import java.awt.Graphics2D;
026import java.awt.RenderingHints;
027import java.awt.image.BufferedImage;
028import java.io.ByteArrayOutputStream;
029import java.io.File;
030import java.io.FileOutputStream;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.OutputStream;
034import java.io.UnsupportedEncodingException;
035import java.net.URLDecoder;
036import java.net.URLEncoder;
037import java.util.ArrayList;
038import java.util.List;
039import java.util.Locale;
040import javax.imageio.ImageIO;
041import javax.servlet.ServletException;
042import javax.servlet.annotation.MultipartConfig;
043import javax.servlet.annotation.WebServlet;
044import javax.servlet.http.HttpServlet;
045import javax.servlet.http.HttpServletRequest;
046import javax.servlet.http.HttpServletResponse;
047import jmri.InstanceManager;
048import jmri.jmrit.roster.Roster;
049import jmri.jmrit.roster.RosterEntry;
050import jmri.server.json.JSON;
051import jmri.server.json.JsonException;
052import jmri.server.json.roster.JsonRosterServiceFactory;
053import jmri.util.FileUtil;
054import jmri.web.servlet.ServletUtil;
055import org.jdom2.JDOMException;
056import org.openide.util.lookup.ServiceProvider;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060/**
061 * Provide roster data to HTTP clients.
062 * <p>
063 * Each method of this Servlet responds to a unique URL pattern.
064 *
065 * @author Randall Wood
066 */
067/*
068 * TODO: Implement an XSLT that respects newlines in comments.
069 * TODO: Include decoder defs and CVs in roster entry response.
070 *
071 */
072@MultipartConfig
073@WebServlet(name = "RosterServlet",
074        urlPatterns = {
075            "/roster", // default
076            "/prefs/roster.xml", // redirect to /roster?format=xml since ~ 9 Apr 2012
077        })
078@ServiceProvider(service = HttpServlet.class)
079public class RosterServlet extends HttpServlet {
080
081    private transient ObjectMapper mapper;
082
083    private final static Logger log = LoggerFactory.getLogger(RosterServlet.class);
084
085    @Override
086    public void init() throws ServletException {
087        if (this.getServletContext().getContextPath().equals("/roster")) { // NOI18N
088            this.mapper = new ObjectMapper();
089        }
090    }
091
092    /**
093     * Route the request and response to the appropriate methods.
094     *
095     * @param request  servlet request
096     * @param response servlet response
097     * @throws java.io.IOException if communications is cut with client
098     */
099    @Override
100    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
101        if (request.getRequestURI().startsWith("/prefs/roster.xml")) { // NOI18N
102            response.sendRedirect("/roster?format=xml"); // NOI18N
103            return;
104        }
105        if (request.getPathInfo().length() == 1) {
106            this.doList(request, response);
107        } else {
108            // split the path after removing the leading /
109            String[] pathInfo = request.getPathInfo().substring(1).split("/"); // NOI18N
110            switch (pathInfo[0]) {
111                case LIST:
112                    this.doList(request, response);
113                    break;
114                case GROUP:
115                    if (pathInfo.length == 2) {
116                        this.doGroup(request, response, pathInfo[1]);
117                    } else {
118                        this.doList(request, response);
119                    }
120                    break;
121                default:
122                    this.doEntry(request, response);
123                    break;
124            }
125        }
126    }
127
128    /**
129     * Handle any POST request as an upload of a roster file from client.
130     *
131     * @param request  servlet request
132     * @param response servlet response
133     * @throws javax.servlet.ServletException if unable to process uploads
134     * @throws java.io.IOException            if communications is cut with
135     *                                        client
136     */
137    @Override
138    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
139
140        OutputStream out = null;
141        InputStream fileContent = null;
142        File rosterFolder = new File(Roster.getDefault().getRosterLocation(), "roster");
143        if (!rosterFolder.exists()) { //insure roster folder exists
144            if (rosterFolder.mkdir()) {
145                log.debug("Roster folder not found, created '{}'", rosterFolder.getPath());
146            } else {
147                log.error("Could not create roster directory: '{}'", rosterFolder.getPath());
148            }
149        }
150        File tempFolder = new File(System.getProperty("java.io.tmpdir"));
151        Locale rl = request.getLocale();
152
153        //get the uploaded file(s)
154        List<FileMeta> files = MultipartRequestHandler.uploadByJavaServletAPI(request);
155
156        List<String> msgList = new ArrayList<>();
157
158        //loop thru files returned and validate and (if ok) save each
159        for (FileMeta fm : files) {
160            log.debug("processing uploaded '{}' file '{}' ({}), group='{}', roster='{}', temp='{}'", fm.getFileType(), fm.getFileName(),
161                    fm.getFileSize(), fm.getRosterGroup(), rosterFolder, tempFolder);
162
163            //only allow xml files or image files
164            if (!fm.getFileType().equals("text/xml")
165                    && !fm.getFileType().startsWith("image")) {
166                String m = String.format(rl, Bundle.getMessage(rl, "ErrorInvalidFileType"), fm.getFileName(), fm.getFileType());
167                log.error("{} : Invalid File Type", m);
168                msgList.add(m);
169                break; //stop processing this one
170            }
171            //save received file to temporary folder
172            File fileTemp = new File(tempFolder, fm.getFileName());
173            try {
174                out = new FileOutputStream(fileTemp);
175                fileContent = fm.getContent();
176                int read;
177                final byte[] bytes = new byte[1024];
178                while ((read = fileContent.read(bytes)) != -1) {
179                    out.write(bytes, 0, read);
180                }
181                log.debug("file '{}' of type '{}' temp saved to {}", fm.getFileType(), fm.getFileName(), tempFolder);
182            } catch (IOException e) {
183                String m = String.format(rl, Bundle.getMessage(rl, "ErrorSavingFile"), fm.getFileName());
184                log.error("{} : Error Saving File", m);
185                msgList.add(m);
186                break; //stop processing this one
187            } finally {
188                if (out != null) {
189                    out.close();
190                }
191                if (fileContent != null) {
192                    fileContent.close();
193                }
194            } //finally
195
196            //reference to target file name and location
197            File fileNew = new File(rosterFolder, fm.getFileName());
198
199            //save image file, replacing if parm is set that way. return appropriate message
200            if (fm.getFileType().startsWith("image")) {
201                if (fileNew.exists()) {
202                    if (!fm.getFileReplace()) {
203                        String m = String.format(rl, Bundle.getMessage(rl, "ErrorFileExists"), fm.getFileName());
204                        log.error("{} : File Already Exists", m);
205                        msgList.add(m);
206                        if (!fileTemp.delete()) { //get rid of temp file
207                            log.error("Unable to delete {}", fileTemp);
208                        }
209                    } else {
210                        if (!fileNew.delete()) { //delete the old file
211                            String m = String.format(rl, Bundle.getMessage(rl, "ErrorDeletingFile"), fileNew.getName());
212                            log.debug("{} : Error Deleting File", m);
213                            msgList.add(m);
214                        }
215                        if (fileTemp.renameTo(fileNew)) {
216                            String m = String.format(rl, Bundle.getMessage(rl, "FileReplaced"), fm.getFileName());
217                            log.debug("{} : File Replaced", m);
218                            msgList.add(m);
219                        } else {
220                            String m = String.format(rl, Bundle.getMessage(rl, "ErrorRenameFailed"), fm.getFileName());
221                            log.error("{} : Rename Failed", m);
222                            msgList.add(m);
223                            if (!fileTemp.delete()) { //get rid of temp file
224                                log.error("Unable to delete {}", fileTemp);
225                            }
226                        }
227                    }
228                } else {
229                    if (fileTemp.renameTo(fileNew)) {
230                        String m = String.format(rl, Bundle.getMessage(rl, "FileAdded"), fm.getFileName());
231                        log.debug("{} : File Added", m);
232                        msgList.add(m);
233                    } else {
234                        String m = String.format(rl, Bundle.getMessage(rl, "ErrorRenameFailed"), fm.getFileName());
235                        log.error("{} : Rename Failed", m);
236                        msgList.add(m);
237                        if (!fileTemp.delete()) { //get rid of temp file
238                            log.error("Unable to delete {}", fileTemp);
239                        }
240                    }
241
242                }
243            } else {
244                RosterEntry reTemp; // create a temp rosterentry to check, based on uploaded file
245                try {
246                    reTemp = RosterEntry.fromFile(new File(tempFolder, fm.getFileName()));
247                } catch (JDOMException e) { //handle XML failures
248                    String m = String.format(rl, Bundle.getMessage(rl, "ErrorInvalidXML"), fm.getFileName(), e.getMessage());
249                    log.error("{} : Invalid XML", m);
250                    msgList.add(m);
251                    if (!fileTemp.delete()) { //get rid of temp file
252                        log.error("Unable to delete {}", fileTemp);
253                    }
254                    break;
255                }
256                RosterEntry reOld = Roster.getDefault().getEntryForId(reTemp.getId()); //get existing entry if found
257                if (reOld != null) {
258                    if (!fm.getFileReplace()) {
259                        String m = String.format(rl, Bundle.getMessage(rl, "ErrorFileExists"), fm.getFileName());
260                        log.error("{} : File Already Exists", m);
261                        msgList.add(m);
262                        if (!fileTemp.delete()) { //get rid of temp file
263                            log.error("Unable to delete {}", fileTemp);
264                        }
265                    } else { //replace specified
266                        Roster.getDefault().removeEntry(reOld); //remove the old entry from roster
267                        reTemp.updateFile(); //saves XML file to roster folder and makes backup
268                        Roster.getDefault().addEntry(reTemp); //add the new entry to roster
269                        Roster.getDefault().writeRoster(); //save modified roster.xml file
270                        String m = String.format(rl, Bundle.getMessage(rl, "RosterEntryReplaced"), fm.getFileName(), reTemp.getDisplayName());
271                        log.debug("{} : Roster Entry Replaced", m);
272                        msgList.add(m);
273                        if (!fileTemp.delete()) { //get rid of temp file
274                            log.error("Unable to delete {}", fileTemp);
275                        }
276                    }
277                } else {
278                    if (fileTemp.renameTo(fileNew)) { //move the file to proper roster location
279                        Roster.getDefault().addEntry(reTemp);
280                        Roster.getDefault().writeRoster();
281                        String m = String.format(rl, Bundle.getMessage(rl, "RosterEntryAdded"), fm.getFileName(), reTemp.getId());
282                        log.debug("{} : Roster Entry Added", m);
283                        msgList.add(m);
284                    } else {
285                        String m = String.format(rl, Bundle.getMessage(rl, "ErrorMoveFailed"), fm.getFileName(), reTemp.getPathName());
286                        log.error("{} : File Move Failed", m);
287                        msgList.add(m);                        
288                    }
289                }
290
291            }
292
293        } //for FileMeta
294
295        //respond with a json list of messages from the upload attempts
296        response.setContentType("application/json");
297        mapper.writeValue(response.getOutputStream(), msgList);
298    }
299
300    /**
301     * Get a roster group.
302     * <p>
303     * Lists roster entries in the specified group and return an XML document
304     * conforming to the JMRI JSON schema. This method can be passed multiple
305     * filters matching the filter in {@link jmri.jmrit.roster.Roster#getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
306     * }. <b>Note:</b> Any given filter can be specified only once.
307     * <p>
308     * This method responds to the following GET URL patterns: <ul>
309     * <li>{@code/roster/group/<group name>}</li>
310     * <li>{@code/roster/group/<group name>?filter=filter[&filter=filter]}</li>
311     * </ul>
312     * <p>
313     * This method responds to the POST URL {@code/roster/group/<group name>}
314     * with a JSON payload for the filter.
315     *
316     * @param request  servlet request
317     * @param response servlet response
318     * @param group    The group name
319     * @throws java.io.IOException if communications is cut with client
320     */
321    protected void doGroup(HttpServletRequest request, HttpServletResponse response, String group) throws IOException {
322        log.debug("Getting group {}", group);
323        ObjectNode data;
324        if (request.getContentType() != null && request.getContentType().contains(UTF8_APPLICATION_JSON)) {
325            data = (ObjectNode) this.mapper.readTree(request.getReader());
326            if (data.path(DATA).isObject()) {
327                data = (ObjectNode) data.path(DATA);
328            }
329        } else {
330            data = this.mapper.createObjectNode();
331            for (String filter : request.getParameterMap().keySet()) {
332                if (filter.equals(ID)) {
333                    data.put(NAME, URLDecoder.decode(request.getParameter(filter), UTF8));
334                } else {
335                    data.put(filter, URLDecoder.decode(request.getParameter(filter), UTF8));
336                }
337            }
338        }
339        data.put(GROUP, URLDecoder.decode(group, UTF8));
340        log.debug("Getting roster with {}", data);
341        this.doRoster(request, response, data);
342    }
343
344    /**
345     * List roster entries.
346     * <p>
347     * Lists roster entries and return an XML document conforming to the JMRI
348     * Roster XML schema. This method can be passed multiple filter filter
349     * matching the filter in
350     * {@link jmri.jmrit.roster.Roster#getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)}.
351     * <b>Note:</b> Any given filter can be specified only once.
352     * <p>
353     * This method responds to the following GET URL patterns: <ul>
354     * <li>{@code/roster/}</li> <li>{@code/roster/list}</li>
355     * <li>{@code/roster/list?filter=filter[&filter=filter]}</li> </ul>
356     *
357     * @param request  servlet request
358     * @param response servlet response
359     * @throws java.io.IOException if communications is cut with client
360     */
361    protected void doList(HttpServletRequest request, HttpServletResponse response) throws IOException {
362        ObjectNode data;
363        if (request.getContentType() != null && request.getContentType().contains(UTF8_APPLICATION_JSON)) {
364            data = (ObjectNode) this.mapper.readTree(request.getReader());
365            if (data.path(DATA).isObject()) {
366                data = (ObjectNode) data.path(DATA);
367            }
368        } else {
369            data = this.mapper.createObjectNode();
370            for (String filter : request.getParameterMap().keySet()) {
371                switch (filter) {
372                    case GROUP:
373                        String group = URLDecoder.decode(request.getParameter(filter), UTF8);
374                        if (!group.equals(Roster.allEntries(request.getLocale()))) {
375                            data.put(GROUP, group);
376                        }
377                        break;
378                    case ID:
379                        data.put(NAME, URLDecoder.decode(request.getParameter(filter), UTF8));
380                        break;
381                    default:
382                        data.put(filter, URLDecoder.decode(request.getParameter(filter), UTF8));
383                        break;
384                }
385            }
386        }
387        this.doRoster(request, response, data);
388    }
389
390    /**
391     * Provide the XML representation of a roster entry given its ID.
392     * <p>
393     * Lists roster entries and return an XML document conforming to the JMRI
394     * Roster XML schema. Requests for roster entry images and icons can include
395     * width and height specifiers, and always return PNG images.
396     * <p>
397     * This method responds to the following URL patterns: <ul>
398     * <li>{@code/roster/<ID>}</li> <li>{@code/roster/entry/<ID>}</li>
399     * <li>{@code/roster/<ID>/image}</li> <li>{@code/roster/<ID>/icon}</li></ul>
400     * <b>Note:</b> The use of the term <em>entry</em> in URLs is optional.
401     * <p>
402     * Images and icons can be rescaled using the following parameters:<ul>
403     * <li>height</li> <li>maxHeight</li> <li>minHeight</li> <li>width</li>
404     * <li>maxWidth</li> <li>minWidth</li></ul>
405     *
406     * @param request  servlet request
407     * @param response servlet response
408     * @throws java.io.IOException if communications is cut with client
409     */
410    protected void doEntry(HttpServletRequest request, HttpServletResponse response) throws IOException {
411        String[] pathInfo = request.getRequestURI().substring(1).split("/");
412        int idOffset = 1;
413        String type = null;
414        if (pathInfo[1].equals("entry")) {
415            if (pathInfo.length == 2) {
416                // path must be /roster/<id> or /roster/entry/<id>
417                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
418            }
419            idOffset = 2;
420        }
421        String id = URLDecoder.decode(pathInfo[idOffset], UTF8);
422        if (pathInfo.length > (1 + idOffset)) {
423            type = pathInfo[pathInfo.length - 1];
424        }
425        RosterEntry re = Roster.getDefault().getEntryForId(id);
426        try {
427            if (re == null) {
428                response.sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find roster entry " + id);
429            } else if (type == null || type.equals("entry")) {
430                // this should be an entirely different format than the table
431                this.doRoster(request, response, this.mapper.createObjectNode().put(ID, id));
432            } else if (type.equals(JSON.IMAGE)) {
433                if (re.getImagePath() != null) {
434                    this.doImage(request, response, FileUtil.getFile(re.getImagePath()));
435                } else {
436                    response.sendError(HttpServletResponse.SC_NOT_FOUND);
437                }
438            } else if (type.equals(JSON.ICON)) {
439                int function = -1;
440                if (pathInfo.length != (2 + idOffset)) {
441                    function = Integer.parseInt(pathInfo[pathInfo.length - 2].substring(1));
442                }
443                if (function == -1) {
444                    if (re.getIconPath() != null) {
445                        this.doImage(request, response, FileUtil.getFile(re.getIconPath()));
446                    } else {
447                        response.sendError(HttpServletResponse.SC_NOT_FOUND);
448                    }
449                } else if (re.getFunctionImage(function) != null) {
450                    this.doImage(request, response, FileUtil.getFile(re.getFunctionImage(function)));
451                } else {
452                    response.sendError(HttpServletResponse.SC_NOT_FOUND);
453                }
454            } else if (type.equals(JSON.SELECTED_ICON)) {
455                if (pathInfo.length != (2 + idOffset)) {
456                    int function = Integer.parseInt(pathInfo[pathInfo.length - 2].substring(1));
457                    this.doImage(request, response, FileUtil.getFile(re.getFunctionSelectedImage(function)));
458                }
459            } else if (type.equals("file")) {
460                InstanceManager.getDefault(ServletUtil.class).writeFile(response, new File(Roster.getDefault().getRosterLocation(), "roster" + File.separator + re.getFileName()), ServletUtil.UTF8_APPLICATION_XML); // NOI18N
461            } else if (type.equals("throttle")) {
462                InstanceManager.getDefault(ServletUtil.class).writeFile(response, new File(FileUtil.getUserFilesPath(), "throttle" + File.separator + id + ".xml"), ServletUtil.UTF8_APPLICATION_XML); // NOI18N
463            } else {
464                // don't know what to do
465                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
466            }
467        } catch (NullPointerException ex) {
468            // triggered by instanciating a File with null path
469            // this would happen when an image or icon is requested for a
470            // rosterEntry that has no such image or icon associated with it
471            response.sendError(HttpServletResponse.SC_NOT_FOUND);
472        }
473    }
474
475    /**
476     * Generate the JSON, XML, or HTML output specified by {@link #doList(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)},
477     * {@link #doGroup(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.String)},
478     * or
479     * {@link #doEntry(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}.
480     *
481     * @param request  servlet request with format and locale for response
482     * @param response servlet response
483     * @param filter   a JSON object with name-value pairs of parameters for
484     *                 {@link jmri.jmrit.roster.Roster#getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)}.
485     * @throws java.io.IOException if communications is cut with client
486     */
487    protected void doRoster(HttpServletRequest request, HttpServletResponse response, JsonNode filter) throws IOException {
488        InstanceManager.getDefault(ServletUtil.class).setNonCachingHeaders(response);
489        log.debug("Getting roster with filter {}", filter);
490        String group = (!filter.path(GROUP).isMissingNode()) ? filter.path(GROUP).asText() : null;
491        log.debug("Group {} was in filter", group);
492
493        String format = request.getParameter(FORMAT);
494        if (format == null) {
495            format = "";
496        }
497        switch (format) {
498            case JSON.JSON:
499                response.setContentType(UTF8_APPLICATION_JSON);
500                JsonRosterServiceFactory factory = new JsonRosterServiceFactory();
501                try {
502                    response.getWriter().print(factory.getHttpService(mapper, V5).getRoster(request.getLocale(), filter, 0));
503                } catch (JsonException ex) {
504                    response.sendError(ex.getCode(), mapper.writeValueAsString(ex.getJsonMessage()));
505                }
506                break;
507            case JSON.XML:
508                response.setContentType(UTF8_APPLICATION_XML);
509                File roster = new File(Roster.getDefault().getRosterIndexPath());
510                if (roster.exists()) {
511                    response.getWriter().print(FileUtil.readFile(roster));
512                }
513                break;
514            case "html":
515                String row;
516                if ("simple".equals(request.getParameter("view"))) {
517                    row = FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(request.getLocale(), "SimpleTableRow.html")));
518                } else {
519                    row = FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(request.getLocale(), "TableRow.html")));
520                }
521                StringBuilder builder = new StringBuilder();
522                response.setContentType(UTF8_TEXT_HTML); // NOI18N
523                if (Roster.allEntries(request.getLocale()).equals(group)) {
524                    group = null;
525                }
526                List<RosterEntry> entries = Roster.getDefault().getEntriesMatchingCriteria(
527                        (!filter.path(ROAD).isMissingNode()) ? filter.path(ROAD).asText() : null,
528                        (!filter.path(NUMBER).isMissingNode()) ? filter.path(NUMBER).asText() : null,
529                        (!filter.path(ADDRESS).isMissingNode()) ? filter.path(ADDRESS).asText() : null,
530                        (!filter.path(MFG).isMissingNode()) ? filter.path(MFG).asText() : null,
531                        (!filter.path(DECODER_MODEL).isMissingNode()) ? filter.path(DECODER_MODEL).asText() : null,
532                        (!filter.path(DECODER_FAMILY).isMissingNode()) ? filter.path(DECODER_FAMILY).asText() : null,
533                        (!filter.path(NAME).isMissingNode()) ? filter.path(NAME).asText() : null,
534                        group
535                );
536                for (RosterEntry entry : entries) {
537                    try {
538                        // NOTE: changing the following order will break JavaScript and HTML code
539                        builder.append(String.format(request.getLocale(), row,
540                                entry.getId(),
541                                entry.getRoadName(),
542                                entry.getRoadNumber(),
543                                entry.getMfg(),
544                                entry.getModel(),
545                                entry.getOwner(),
546                                entry.getDccAddress(),
547                                entry.getDecoderModel(),
548                                entry.getDecoderFamily(),
549                                entry.getDecoderComment(),
550                                entry.getComment(),
551                                entry.getURL(),
552                                entry.getMaxSpeedPCT(),
553                                entry.getFileName(),
554                                URLEncoder.encode(entry.getId(), UTF8)
555                        // get function buttons in a formatting loop
556                        // get attributes in a formatting loop
557                        ));
558                    } catch (UnsupportedEncodingException ex) {
559                        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to encode entry Id in UTF-8."); // NOI18N
560                    }
561                }
562                response.getWriter().print(builder.toString());
563                break;
564            default:
565                if (group == null) {
566                    group = Roster.allEntries(request.getLocale());
567                }
568                response.setContentType(UTF8_TEXT_HTML); // NOI18N
569                response.getWriter().print(String.format(request.getLocale(),
570                        FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(request.getLocale(), "Roster.html"))),
571                        String.format(request.getLocale(),
572                                Bundle.getMessage(request.getLocale(), "HtmlTitle"),
573                                InstanceManager.getDefault(ServletUtil.class).getRailroadName(false),
574                                Bundle.getMessage(request.getLocale(), "RosterTitle")
575                        ),
576                        InstanceManager.getDefault(ServletUtil.class).getNavBar(request.getLocale(), request.getContextPath()),
577                        InstanceManager.getDefault(ServletUtil.class).getRailroadName(false),
578                        InstanceManager.getDefault(ServletUtil.class).getFooter(request.getLocale(), request.getContextPath()),
579                        group
580                ));
581                break;
582        }
583    }
584
585    /**
586     * Process the image for a roster entry image or icon request.
587     *
588     * @param file     {@link java.io.File} object containing an image
589     * @param request  contains parameters for drawing the image
590     * @param response sends a PNG image or a 403 Not Found error.
591     * @throws java.io.IOException if communications is cut with client
592     */
593    void doImage(HttpServletRequest request, HttpServletResponse response, File file) throws IOException {
594        BufferedImage image;
595        try {
596            image = ImageIO.read(file);
597        } catch (IOException ex) {
598            // file not found or unreadable
599            response.sendError(HttpServletResponse.SC_NOT_FOUND);
600            return;
601        }
602        String fname = file.getName();
603        int height = image.getHeight();
604        int width = image.getWidth();
605        int pWidth = width;
606        int pHeight = height;
607        if (request.getParameter("maxWidth") != null) {
608            pWidth = Integer.parseInt(request.getParameter("maxWidth"));
609            if (pWidth < width) {
610                width = pWidth;
611            }
612            log.debug("{} @maxWidth: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
613        }
614        if (request.getParameter("minWidth") != null) {
615            pWidth = Integer.parseInt(request.getParameter("minWidth"));
616            if (pWidth > width) {
617                width = pWidth;
618            }
619            log.debug("{} @minWidth: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
620        }
621        if (request.getParameter("width") != null) {
622            width = Integer.parseInt(request.getParameter("width"));
623        }
624        if (width != image.getWidth()) {
625            height = (int) (height * (1.0 * width / image.getWidth()));
626            pHeight = height;
627            log.debug("{} @adjusting height: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
628        }
629        if (request.getParameter("maxHeight") != null) {
630            pHeight = Integer.parseInt(request.getParameter("maxHeight"));
631            if (pHeight < height) {
632                height = pHeight;
633            }
634            log.debug("{} @maxHeight: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
635        }
636        if (request.getParameter("minHeight") != null) {
637            pHeight = Integer.parseInt(request.getParameter("minHeight"));
638            if (pHeight > height) {
639                height = pHeight;
640            }
641            log.debug("{} @minHeight: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
642        }
643        if (request.getParameter("height") != null) {
644            height = Integer.parseInt(request.getParameter("height"));
645            log.debug("{} @height: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
646        }
647        if (height != image.getHeight() && width == image.getWidth()) {
648            width = (int) (width * (1.0 * height / image.getHeight()));
649            log.debug("{} @adjusting width: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
650        }
651        log.debug("{} @responding: width: {}, pWidth: {}, height: {}, pHeight: {}", fname, width, pWidth, height, pHeight);
652        ByteArrayOutputStream baos = new ByteArrayOutputStream();
653        if (height != image.getHeight() || width != image.getWidth()) {
654            BufferedImage resizedImage = new BufferedImage(width, height, image.getType());
655            Graphics2D g = resizedImage.createGraphics();
656            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
657            g.drawImage(image, 0, 0, width, height, 0, 0, image.getWidth(), image.getHeight(), null);
658            g.dispose();
659            // ImageIO needs the simple type ("jpeg", "png") instead of the mime type ("image/jpeg", "image/png")
660            ImageIO.write(resizedImage, "png", baos);
661        } else {
662            ImageIO.write(image, "png", baos);
663        }
664        baos.close();
665        response.setContentType(IMAGE_PNG);
666        response.setStatus(HttpServletResponse.SC_OK);
667        response.setContentLength(baos.size());
668        response.getOutputStream().write(baos.toByteArray());
669        response.getOutputStream().close();
670    }
671}