TIM 1: Java Servlets & JSP – Menedżer plików

W tym wpisie zaprezentowany zostanie proces stworzenia przykładowego rozwiązania w technologii JSP & Java Servlets.

Wymagania

Końcowe rozwiązanie będzie spełniać następujące wymagania:

  • Aplikacja musi zapewniać widok, wykonujący logikę biznesową,
  • Aplikacja musi być udostępniać widok pozwalający na edycję wszystkich etykiet,
  • Aplikacja musi udostępniać widok pokazujący historię operacji wykonanych w środowisku,
  • Aplikacja musi być konfigurowalna, tj. aplikacja zapewnia widok, pozwalający na edycję co najmniej czterech parametrów aplikacji, np. format zapisu pliku z historią (co najmniej 4 parametry),
  • Aplikacja musi zawierać własny tag JSP.

Za punkt wyjściowy uznajemy utworzony projekt i skonfigurowane środowisko zgodnie z wpisem TiM_0.

Logika biznesowa

Aplikacja, której utworzenie zostanie opisane w tym poście będzie pozwalała na:

  • podgląd struktury plików,
  • przesyłanie/pobieranie plików,
  • tworzenie folderów

Servlet’y

W celu obsługi logiki biznesowej zostaną utworzone cztery servlety:

  • ListFilesServlet
  • UploadFileServlet
  • DownloadFileServlet
  • CreateFolderServlet

Aby utworzyć nowy Servlet w środowisku IntelliJ pierwszym krokiem jest kliknięcie PPM na folder src, a następnie wybranie z menu kontekstowego opcji New.. -> Servlet. Następnie w polu ‚Name’ należy podać nazwę Servlet’u (w tym wypadku ListFilesServlet), a w polu package pakiet w jakim zamierzamy go umieścić (w tym wypadku pl.edu.wat.wcy.jsp.servlet). Po zatwierdzeniu operacji powinniśmy uzyskać kod, jak poniżej:

package pl.edu.wat.wcy.jsp.servlet;

@WebServlet(name = "ListFilesServlet")
public class ListFilesServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
    }
}
ListFilesServlet.java

Następnie, aby umożliwić dostęp do servlet’u z zewnątrz należy nadać mu jakiś wzór akceptowalnych URL’i.

@WebServlet(name = "ListFilesServlet",urlPatterns = "/list-files")
public class ListFilesServlet extends HttpServlet {
  ...
}

Servlet ten za argument będzie przyjmował ścieżkę folderu, a zwracał jego zawartość w formie nazwa, typ (folder, plik), wielkość (jeśli to plik, w MB). Aby pobrać argument z żądania wykorzystywana jest metoda getParameter(String), klasy HttpServletRequest. Do zwracania wyniku z servlet’u wykorzystywany jest obiekt klasy PrintWriter. Zatem deklaracja metody doGet servlet’u zwracającego argument path wygląda następująco:

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        PrintWriter writer = response.getWriter();
        String path = request.getParameter("path");
        writer.write("Path = "+path);
    }

Po uruchomieniu aplikacji i wejściu pod link http://localhost:8080/JavaWeb_war_exploded/list-files?path=/test, wyświetla się teraz:

Path = /test

Teraz w metodzie doGet pozostało zwrócić zawartość folderu path. W tym celu wywołana zostanie metoda zwracająca zawartość katalogu w postaci listy tablic stringów, bądź rzucająca wyjątek jeśli ścieżka jest niepoprawna. Metoda i jej wykorzystanie zostały zaprezentowane poniżej:

    private ArrayList<FileData> getFiles(String path) throws NotDirectoryException {
        File root = new File(path);
        ArrayList<FileData> list = new ArrayList<>();
        if (root.isDirectory()) {
            for (File file : root.listFiles()) {
                list.add(new FileData(file));
            }
        } else {
            throw new NotDirectoryException(path);
        }
        Collections.sort(list);
        return list;
    }
metoda getFiles(String)

Gdzie klasa FileData wykorzystywana jest w celu opsiu modelu danych i wygląda następująco:

package pl.edu.wat.wcy.jsp.model;

public class FileData implements Comparable<FileData> {
    private String fileName;
    private FileType fileType;
    private int fileSize;

    public FileData(File f) {
        fileName = f.getName();
        fileType = f.isDirectory() ? FileType.DIRECTORY : FileType.FILE;
        fileSize = (int) (f.length() / 1024 / 1024);
    }

    public String toString() {
        String result = fileType + " " + fileName;
        if (fileType == FileType.FILE)
            result += " " + fileSize + "(MB)";
        return result;
    }

    @Override
    public int compareTo(FileData o) {
        if (o.fileType != this.fileType) {
            if (this.fileType == FileType.DIRECTORY)
                return -1;
            else return 1;
        }
        return this.fileName.compareTo(o.fileName);
    }

    enum FileType {DIRECTORY, FILE}
    ...
}
FileData.java

Ostatecznie wykorzystanie metody:

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
        PrintWriter writer = response.getWriter();
        String path = request.getParameter("path");
        try {
            ArrayList<FileData> files = getFiles(path);
            for (FileData f : files) {
                writer.write(f.toString() + "\n");
            }
        } catch (NotDirectoryException e) {
            writer.write(path + " is not a valid directory.");
        }
    }
doGet@ListFilesServlet.java

Do przesyłania plików wykorzystamy następujący servlet:

@WebServlet(name = "ListFilesServlet", urlPatterns = "/upload")
@MultipartConfig
public class UploadFileServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String path = request.getParameter("path");
        File root = new File(path);
        if (root.isDirectory()) {
            uploadFile(request,path);
        }
    }
    ...
}
UploadFileServlet.java

Adnotacja @MultipartConfig pozwala na wsparcie formularzy z wieloma polami (MIME type: multipart/form-data) dzięki czemu można wykorzystać metodę getPart(). Ciało metody upload file, wygląda następująco:

public long uploadFile(HttpServletRequest request,String path) 
        throws IOException, ServletException {
    Part filePart = request.getPart("file");
    String fileName = filePart.getSubmittedFileName();
    InputStream fileContent = filePart.getInputStream();
    Path filePath = Paths.get(path, fileName);
    return Files.copy(fileContent, filePath, StandardCopyOption.REPLACE_EXISTING);
}

Do pobierania plików wykorzystany został GetFileServlet.

@WebServlet(name = "DownloadFileServlet",urlPatterns = "/get")
public class DownloadFileServlet extends HttpServlet {

    public void doGet(HttpServletRequest request,
                      HttpServletResponse response) throws IOException{
        (...)
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "filename=\""+file+"\"");
        File root = new File(path);
        if(root.isDirectory())
        {
            Path srcPath = Paths.get(path,file);
            Files.copy(srcPath, response.getOutputStream());
            response.getOutputStream().flush();
        }
        else response.setStatus(404);
    }
}
GetFileServlet.java

Ponadto każdy servlet posiada fragment kodu odpowiedzialny za działanie w przypadku braku dostarczenia parametrów, działający w sposób zbliżony do poniższego listing’u:

        if(path==null || file ==null)
        {
            response.setStatus(404);
            return;
        }

Edycja etykiet

W celu umożliwienia edycji etykiet stworzony została klasa zgodna z wzorcem singleton zarządzająca dostępem do etykiet. Etykiety przechowywane są w pliku label.properties i wczytywane są przy uruchomieniu aplikacji. Plik zapisywany jest na nowo po każdej zmianie etykiet.

public class LabelSingleton {
    private static final String FILE = "labels.properties";
    private static LabelSingleton INSTANCE;
    private HashMap<String, String> labels = new HashMap<>();
    private Properties properties;
    public LabelSingleton() {
        properties = new Properties();
        try {
            properties.load(getClass().getResourceAsStream(FILE));
            for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
                labels.put((String) entry.getKey(), (String) entry.getValue());
            }
        } catch (Exception e) {
        }
    }

    public static synchronized LabelSingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LabelSingleton();
        }
        return INSTANCE;
    }
    (...)
}
LabelSingleton.java

Za dostęp do etykiet odpowiadają metody getLabel oraz setLabel:

    public static String getLabel(String key) {
        LabelSingleton ls = getInstance();
        String label = key;
        if (ls.labels.containsKey(key))
            label = ls.labels.get(key);
        return label;
    }
    public static void setLabel(String key,String value) {
        LabelSingleton ls = getInstance();
        ls.labels.put(key,value);
        ls.properties.put(key,value);
    }

Za zapis do pliku odpowiada metoda store():

    public static void store(){
        LabelSingleton ls = getInstance();
        try {
            PrintWriter writer =
                    new PrintWriter(
                            new File(ls.getClass().getResource(FILE).getPath()));
            ls.properties.store(writer,null);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Plik do przechowywania etykiet należy umieścić w klasie w pakiecie w którym znajduje się klasa odpowiadająca za dostęp do nich.

Własny tag JSP

W celu wykorzystania etykiet w widokach utworzymy własny tag JSP. Pierwszym krokiem jest utworzenie klasy rozszerzającej SimpleTagSupport, która w metodzie doTag() zwróci tekst dla etykiety podanej jako atrybut. Aby uzyskać dostęp do atrybutu należy w klasie obsługującej dodać dla niego setter.

public class LocalizedTag extends SimpleTagSupport{

    private String key;

    public void setKey(String key) {
        this.key = key;
    }

    @Override
    public void doTag() throws JspException, IOException {
        getJspContext().getOut().write(LabelSingleton.getLabel(key));
    }
}
LocalizedTag.java

Kolejnym krokiem jest utworzenie w folderze META-INF folderu tlds w którym będą przetrzymywane opisy naszych tag’ów. Później należy nacisnąć prawym przyciskiem myszy na nowo utworzonym folderze i z menu wybrać opcję New.. -> XML Configuration File -> JSP Tag Library Descriptor. Następnie wewnątrz utworzonego pliku klikamy prawym -> Generate.. i wybieramy klasę rozszerzającą SimpleTagSupport. Body-content wybieramy zgodnie z dokumentacją – w tym przypadku empty. Następnie definiujemy atrybut jako <attribute>. Uzyskany plik xml ukazano poniżej:

<taglib xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd" version="2.1">
    <tlib-version>1.0</tlib-version>
    <short-name>Cutsom</short-name>
    <uri>custom.tld</uri>
    <tag>
        <name>localizedTag</name>
        <tag-class>pl.edu.wat.wcy.jsp.tags.LocalizedTag</tag-class>
        <body-content>empty</body-content>
        <attribute>
            <name>key</name>
        </attribute>
    </tag>
</taglib>
custom.tld

Wykorzystanie własnego tag’u JSP

W celu wykorzystania własnego tag’u JSP należy zdefiniować wykorzystywany prefix – tutaj wykorzystano „c”.

<%@ taglib prefix="c" uri="custom.tld"%>
Definicja prefix'u

Następnie wykorzystując składnię prefix:tag wykorzystujemy tag w reszcie kodu strony JSP:

      <c:localizedTag key="Test"/>
Wykorzystanie tagu

Konfigurowalność aplikacji

Konfigurowalność aplikacji można zapewnić analogicznie do edycji etykiet.

Historia wykonanych operacji

W celu zapewnienia historii wykonanych operacji można utworzyć analogiczny singleton posiadający metodę log(), zapisujący dane o wykonanej operacji na koniec pliku.