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.