Kihagyás

10. gyakorlat

JSP folytatás

Fragment-ek

A nem teljes értékű, csak töredék JSP oldalakat JSP fragment-eknek nevezzük.

Feladat

A person-administrator alkalmazásunk jsp/servlet megvalósításában a menu és a közös header elemek nem önálló JSP oldalak. Viszont a böngészőből direktben elérhetőek ezek az oldalak. Javítsuk ki, hogy erre ne legyen lehetőség! Arról már volt szó, hogy amit nem akarunk, hogy a felhasználó elérjen, azt a WEB-INF könyvtárba kell pakolni. Tegyünk is így: mozgassuk a teljes common mappát a WEB-INF alá! Ezután állítsuk át a többi oldalon szereplő elérési útvonalakat (../WEB-INF/common/... kell legyen)!

Felhasználók kezelése

Feladat

Adjunk hozzá az alkalmazásunkhoz valódi felhasználó kezelést! Jelen esetben a login alkalmával csak beletesszük a session-be és kész. Legyen lehetőségünk úgy belépni, hogy ténylegesen ellenőrizzük az alkalmazásban, hogy helyes felhasználó jelszó párost adott-e meg a felhasználó. A jelszavakat titkosítsuk bcrypt algoritmussal! Ehhez konstruáljuk egy felhasználó táblát, illetve az adott felhasználó külön tudja menedzselni a saját kontaktjait, tehát minden felhasználó csak az általa hozzáadott embereket látja. Legyen egy regisztráció menüpont is a login-ról elérhető!

  1. lépés: Dolgozzuk át az adatbázist! Az valahogy így kell, hogy kinézzen az átalakítás után:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
     create table CONTACT
     (
         id INTEGER not null
             constraint CONTACT_pk
                 primary key autoincrement,
         name text not null,
         email text not null,
         address text,
         dateOfBirth text not null,
         company text,
         position text,
         user_id int
     );
    
     create unique index CONTACT_email_uindex
         on CONTACT (email);
    
     create table PHONE
     (
         id integer not null
             constraint PHONE_pk
                 primary key autoincrement,
         number text not null,
         phoneType integer not null,
         contact_id int
             references CONTACT
     );
    
     create table USER
     (
         id INTEGER not null
             constraint USER_pk
                 primary key autoincrement,
         username text not null,
         password text not null,
         email text not null
     );
    
     create unique index USER_email_uindex
         on USER (email);
    
     create unique index USER_username_uindex
         on USER (username);
    
  2. lépés: User model hozzáadása a contacts-core-hoz és Contact model aktualizálása.

    Contact.java

    1
    2
    3
    4
    5
    6
     public class Contact{
         // other properties
         private ObjectProperty<User> user = new SimpleObjectProperty<>(this, "user");
    
         // getters, setters
     }
    

    User.java

    1
    2
    3
    4
    5
    6
    7
    8
     public class User {
         private IntegerProperty id = new SimpleIntegerProperty();
         private StringProperty username = new SimpleStringProperty();
         private StringProperty password = new SimpleStringProperty();
         private StringProperty email = new SimpleStringProperty();
    
         // getters, setters
     }
    

  3. lépés: UserDAO és UserDAOImpl elkészítése

    1
    2
    3
    4
    5
    6
    public interface UserDAO {
    
        User getUserById(int id);
        void addUser(User user);
        User login(String username, String password);
    }
    

    A UserDAO-ban semmilyen különleges nincs. Lehetőséget ad lekérdezésre, hozzáadásra (regisztráció), illetve bejelentkezésre.

    A konkrét megvalósítás:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    public class UserDAOImpl implements UserDAO{
    
        private static UserDAOImpl instance;
        private static final String DB_CONN_STR = ContactConfiguration.getValue("db.url");
    
        public static UserDAOImpl getInstance() {
            if (instance == null) {
                try {
                    Class.forName("org.sqlite.JDBC");
                } catch (ClassNotFoundException e1) {
                    e1.printStackTrace();
                }
                instance = new UserDAOImpl();
            }
            return instance;
        }
    
        private UserDAOImpl() {
        }
    
        @Override
        public User getUserById(int id) {
            try (Connection conn = DriverManager.getConnection(DB_CONN_STR);
                PreparedStatement pst = conn.prepareStatement("SELECT * FROM USER WHERE id = ?")
            ) {
                pst.setInt(1, id);
    
                ResultSet rs = pst.executeQuery();
                if (rs.next()) {
                    User u = new User();
                    u.setId(rs.getInt(1));
                    u.setUsername(rs.getString(2));
                    u.setEmail(rs.getString(3));
                    u.setPassword(rs.getString(4));
                    return u;
                }
    
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public void addUser(User user) {
    
            try (Connection conn = DriverManager.getConnection(DB_CONN_STR);
                PreparedStatement pst = conn.prepareStatement("INSERT INTO USER (username, password, email) VALUES (?,?,?)")
            ) {
    
                String newPwd = BCrypt.withDefaults().hashToString(12, user.getPassword().toCharArray());
                user.setPassword(newPwd);
                pst.setString(1, user.getUsername());
                pst.setString(2, user.getPassword());
                pst.setString(3, user.getEmail());
    
                pst.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public User login(String username, String password) {
    
            try (Connection conn = DriverManager.getConnection(DB_CONN_STR);
                PreparedStatement pst = conn.prepareStatement("SELECT * FROM USER WHERE username = ?")
            ) {
                pst.setString(1, username);
                ResultSet rs = pst.executeQuery();
                if (rs.next()) {
    
                    String dbPass = rs.getString("password");
                    BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), dbPass);
                    if(result.verified){
                        User user = new User();
                        user.setUsername(rs.getString("username"));
                        user.setPassword(rs.getString("password"));
                        user.setEmail(rs.getString("email"));
                        user.setId(rs.getInt("id"));
                        return user;
                    }
                }
    
            } catch (SQLException e) {
                e.printStackTrace();
            }
    
            return null;
        }
    }
    

    Érdemes megfigyelni, hogy a UserDAOImpl példányosítását a Singleton tervezési minta mentén készítettük el, így a getInstance hívással kérhetünk majd egy ilyen példányt (private konstruktor miatt kívülről nem példányosítható). A megismert mintát alkalmazhatjuk a többi DAO osztályon is.

    Egy további érdekes rész, amikor az addUser metódusban a jelszó hash-elt változatát állítjuk elő, tesszük ezt azért, mert nem szeretnénk a natúr szöveges jelszavunkat menteni a DB-be. Ehhez a bcrypt algoritmust használjuk, mely manapság egy javasolt hash-elési algoritmus. A JDK nem tartalmaz erre a célre beépített osztályokat, így egy külön függőséget használnuk.

    1
    2
    3
    4
    5
    <dependency>
      <groupId>at.favre.lib</groupId>
      <artifactId>bcrypt</artifactId>
      <version>0.9.0</version>
    </dependency>
    

    Jelen esetben a az alapbeállításokat használjuk (withDefaults), illetve így állítjuk elő a hash string-et a password alapján. További konfigurációkat is megadhatunk, melyről bővebben a lib github oldalán találunk leírást. Miután megvan a hash, ezt beállítjuk a user password-jének és így ezt mentjük az adatbázisba. Amikor bejelentkezünk, akkor pedig a megadott jelszót veti össze a library az adatbázisban szereplővel: BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), dbPass);

  4. lépés: Contact DAO updatelése

    ContactDAO.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public interface ContactDAO {
    
        List<Contact> findAll();
        List<Contact> findAll(User user);
    
        Contact save(Contact contact);
        void delete(Contact contact);
    
    }
    

    A ContactDAO úgy változik meg, hogy a findAll paraméterében egy User-t is átadunk. A mentéskor szeretnénk jelölni, hogy az újönnan létrehozott kontakt melyik felhasználóhoz tartozik (ezt most már a Contact modellben eltároljuk), illetve, amikor lekérdezünk, akkor egy felhasználóhoz tartozó kontaktot listáját adjuk vissza. Mivel az asztali alkalmazásunkban nem kezeltünk felhasználókat és nem is fogunk már visszamenni arra a pontra, hogy ezt implementáljunk, ezért az eredeti függvénylenyomatokat meghagyjuk és biztosítunk olyan lehetőséget, melyben a User is megadható. Ennek tükrében a ContactDAOImpl a következőképpen néz ki:

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    public class ContactDAOImpl implements ContactDAO{
    
        private static final String SELECT_ALL_CONTACTS = "SELECT * FROM CONTACT";
        private static final String SELECT_ALL_CONTACTS_BY_USER = "SELECT * FROM CONTACT WHERE user_id = ?";
        private static final String INSERT_CONTACT = "INSERT INTO CONTACT (name, email, address, dateOfBirth, company, position, user_id) VALUES (?,?,?,?,?,?,?)";
        private static final String UPDATE_CONTACT = "UPDATE CONTACT SET name=?, email = ?, address = ?, dateOfBirth=?, company=?, position = ? WHERE id=?";
        private static final String DELETE_CONTACT = "DELETE FROM CONTACT WHERE id = ?";
        private String connectionURL;
        private PhoneDAO phoneDAO = new PhoneDAOImpl();
        private UserDAO userDAO = UserDAOImpl.getInstance();
    
        public ContactDAOImpl(){
            try {
                Class.forName("org.sqlite.JDBC");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            connectionURL = ContactConfiguration.getValue("db.url");
        }
    
        @Override
        public List<Contact> findAll() {
            return findAll(null);
        }
    
        @Override
        public List<Contact> findAll(User user) {
            List<Contact> result = new ArrayList<>();
    
            try(Connection c = DriverManager.getConnection(connectionURL);
            ){
                ResultSet rs;
                if(user == null){
                    Statement stmt = c.createStatement();
                    rs = stmt.executeQuery(SELECT_ALL_CONTACTS);
                }
                else{
                    PreparedStatement stmt = c.prepareStatement(SELECT_ALL_CONTACTS_BY_USER);
                    stmt.setInt(1, user.getId());
                    rs = stmt.executeQuery();
                }
    
                while(rs.next()){
                    Contact contact = new Contact();
                    contact.setId(rs.getInt("id"));
                    contact.setName(rs.getString("name"));
                    contact.setEmail(rs.getString("email"));
                    contact.setAddress(rs.getString("address"));
                    Date date = Date.valueOf(rs.getString("dateOfBirth"));
                    contact.setDateOfBirth(date == null ? LocalDate.now() : date.toLocalDate());
                    contact.setCompany(rs.getString("company"));
                    contact.setPosition(rs.getString("position"));
                    contact.setPhones(phoneDAO.findAllByContactId(contact.getId()));
                    contact.setUser(userDAO.getUserById(rs.getInt("user_id")));
                    result.add(contact);
    
                }
    
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
    
            return result;
        }
    
        @Override
        public Contact save(Contact contact) {
            try(Connection c = DriverManager.getConnection(connectionURL);
                PreparedStatement stmt = contact.getId() <= 0 ? c.prepareStatement(INSERT_CONTACT, Statement.RETURN_GENERATED_KEYS) : c.prepareStatement(UPDATE_CONTACT)
            ){
                if(contact.getId() > 0){ // UPDATE
                    stmt.setInt(7, contact.getId());
                }
                else{
                    if(contact.getUser() != null){
                        stmt.setInt(7, contact.getUser().getId());
                    }
                }
    
                stmt.setString(1, contact.getName());
                stmt.setString(2, contact.getEmail());
                stmt.setString(3, contact.getAddress());
                stmt.setString(4, contact.getDateOfBirth().toString());
                stmt.setString(5, contact.getCompany());
                stmt.setString(6, contact.getPosition());
    
                int affectedRows = stmt.executeUpdate();
                if(affectedRows == 0){
                    return null;
                }
    
                if(contact.getId() <= 0){ // INSERT
                    ResultSet genKeys = stmt.getGeneratedKeys();
                    if(genKeys.next()){
                        contact.setId(genKeys.getInt(1));
                    }
                }
    
            } catch (SQLException throwables) {
                throwables.printStackTrace();
                return null;
            }
    
            return contact;
        }
    
        // delete is unmodified
    }
    
  5. lépés: login updatelése

    A login oldalon alapvetően marad minden úgy, ahogy volt de egy linket hozzáadunk, hogy a regisztrálás oldalra át tudjunk navigálni abban az esetben ha még nem volt létrehozott fiókunk.

    1
    <span><a href="register.jsp">Register</a></span>
    
  6. lépés: LoginController updatelése:

    Ahhoz, hogy a bejentkezés az új DAO-val együttműködjön, a LoginController-ben is kell néhány módosítást eszközölnünk. Itt már felhasználjuk a DAO nyújtotta login metódust.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
    
        UserDAO userDAO = UserDAOImpl.getInstance();
    
        String username = request.getParameter("username");
        String password = request.getParameter("password");
    
        User user = userDAO.login(username, password);
    
        if( user == null){
            response.sendRedirect("pages/login.jsp");
            return;
        }
    
        request.getSession().setAttribute("currentUser", user);
        response.sendRedirect("pages/list-contact.jsp");
    }
    
  7. lépés: regisztrációs form

    A regisztrációs form valahogy így nézhet ki:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     <%@ page contentType="text/html;charset=UTF-8" language="java" %>
     <html>
     <head>
         <jsp:include page="../WEB-INF/common/common-header.jsp"/>
         <title>Register</title>
     </head>
     <body>
     <div class="container">
         <form action="../RegisterController" method="post">
             <div class="form-group">
                 <label for="username">Username</label>
                 <input required name="username" type="text" class="form-control" id="username"
                     placeholder="Username"/>
             </div>
             <div class="form-group">
                 <label for="password">Password</label>
                 <input required name="password" type="password" class="form-control" id="password"
                     placeholder="Password"/>
             </div>
             <div class="form-group">
                 <label for="email">Email</label>
                 <input required name="email" type="email" class="form-control" id="email"
                     placeholder="Email"/>
             </div>
             <button id="submit" type="submit" class="btn btn-primary">Submit</button>
         </form>
     </div>
    
     </body>
     </html>
    

    A regisztrációs formban beérjük a felhasználónevet, a jelszót és az email címet. A regisztrációs form az adatokat továbbküldi a RegisterController-nek, mely az adatrétegnek továbbküldi a regisztrációs kérelmet.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @WebServlet("/RegisterController")
    public class RegisterController extends HttpServlet {
    
        UserDAO dao = UserDAOImpl.getInstance();
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            req.setCharacterEncoding("utf-8");
            resp.setCharacterEncoding("utf-8");
            User user = new User();
            user.setUsername(req.getParameter("username"));
            user.setEmail(req.getParameter("email"));
            user.setPassword(req.getParameter("password"));
    
            dao.addUser(user);
    
            resp.sendRedirect("pages/login.jsp");
        }
    }
    
    Jelen esetben semmilyen jellegű ellenőrzés nincs, amivel a regisztráció sikerességét ellenőrizzük.

  8. lépés: Alakítsunk a hozzáadáson és a listázáson az újfajta DAO-nak megfelelően (kell egy user, akihez tartozó kontaktokat szeretnénk listázni, illetve akinek szeretnénk új kontaktot létrehozni).

    Az egyik igen fontos pont, hogy a list-contact.jsp-ben behúzzuk a ContactController-t, melynek ilyen módon a doGet metódusa fog lefutni, mely az alábbit teszi:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    ...
    private ContactDAO dao = new ContactDAOImpl();
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        User currentUser = (User) req.getSession().getAttribute("currentUser");
        List<Contact> all = dao.findAll(currentUser);
        req.setAttribute("contactList", all);
    }
    ...
    

    A session alapján lekérjük az aktuális felhasználót és csak a hozzá tartozó kontaktokat adjuk vissza.

    A hozzáadás form-ja nem változott, csak a common header rész miatt.

    Elküldjük a ContactController-nek az összes szükséges paramétert. A doPost a következőképpen módosul:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        // setup params
        ...
        User currentUser = (User) request.getSession().getAttribute("currentUser");
        c.setUser(currentUser);
    
        c = dao.save(c);
        ...
        // saving phones
    
    }
    

    Fontos, hogy a request encoding-ját is UTF-8-ra állítsuk be, máskülönben az adatbázisba is rossz encoding-al kerül be az új kontakt (request.setCharacterEncoding("utf-8");).

  9. lépés: Lényegében kész vagyunk, de egy-két dolog még kimaradt, mint például a logout átalakítása.

    Eddig a cookie-ban volt csak az aktuális user, most viszont a sessionben tároljuk, így ezt is át kell alakítanunk kicsit.

    1
    2
    3
    4
    5
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.getSession().removeAttribute("username");
    
        response.sendRedirect("pages/login.jsp");
    }
    

    Szimplán töröljük a sessionből a currentUser attribútumot, illetve a login oldalra kalauzoljuk a felhasználót.

  10. lépés: Menü update:

    A menüsáv jobb oldalán kiírjuk az aktuális felhasználó nevét, illetve az itteni lenyíló menüben biztosítjuk, hogy a felhasználó ki tudjon jelentkezni. A menüsáv szintén a cookie-t használta eddig, így ezt is át kell alakítani. Az idevágó kódrészlet:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <c:if test="${sessionScope.currentUser.username != null}">
        <li class="nav-item dropdown ml-auto">
            <a class='nav-link dropdown-toggle' href='#' id='navbarDropdownMenuLink' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
                ${sessionScope.currentUser.username}
            </a>
            <div class='dropdown-menu dropdown-menu-right' aria-labelledby='navbarDropdownMenuLink'>
                <a class='dropdown-item' href='../LogoutController'>Kijelentkezés</a>
            </div>
        </li>
    </c:if>
    
  11. lépés: Filterezés

    A login megoldásunk már egészen tűrhető, azonban jelen helyzetben, ha a valamelyik oldalra navigálunk direktben, akkor az oldalt megpróbálja betölteni a rendszer. A megfelelő működés az volna, hogy legalább a session-t ellenőrizzük, hogy be van-e jelentkezve a felhasználó. Amennyiben igen, akkor az adott oldalra engedjük, máskülönben visszhaírányítjuk a login oldalra. Mivel a filterezést minden lehetséges oldalra szeretnénk végrehajtani, így az a következőképpen néz ki:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @WebFilter("/*")
    public class AuthFilter implements Filter {
    
        private List<String> exclusions;
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            this.exclusions = Arrays.asList(filterConfig.getServletContext().getInitParameter("login-filter-exclusion").split(","));
            this.exclusions.replaceAll(String::trim);
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    
            String path = ((HttpServletRequest) request).getServletPath();
            if (exclusions.stream().anyMatch(path::equals)) {
                chain.doFilter(request, response);
                return;
            }
    
            User currentUser = (User) ((HttpServletRequest) request).getSession().getAttribute("currentUser");
    
            if (currentUser == null) {
                ((HttpServletResponse)response).sendRedirect(((HttpServletRequest) request).getContextPath() + "/pages/login.jsp");
            }
            else{
                chain.doFilter(request, response);
            }
        }
    }
    

    A filterezésnél a /*-al mondjuk meg, hogy minden kérésre szeretnénk futtatni a filtert. Mivel van több oldal is, amin azonban nem szeretnénk, ha a filter lefutna, így ezeket valamilyen módon meg kell adnunk. Sajnos kivételeket nem tudunk megadni a filter url-patter résznél, ezért a web.xml-ben adjuk meg azokat az url-eket, melyekre nem szeretnénk filterezést végrehajtani. Még egy technikai problém, hogy egy param-hoz nem tudunk több értéket megadni, így vesszővel elválasztva adjuk ezt meg. A Filter-ben pedig a vesszők mentén feldaraboljuk a string-et (init-ben).

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    <context-param>
        <param-name>login-filter-exclusion</param-name>
        <param-value>
            /index.jsp,
            /pages/login.jsp,
            /LoginController,
            /pages/register.jsp,
            /RegisterController,
            /css/style.css
        </param-value>
    </context-param>
    

    A doFilter metódusban megvizsgáljuk, hogy az aktuális kérés url-je (context url nélkül) megegyezik-e a megadott lista bármely elemével. Amennyiben találunk ilyet, akkor továbbengedjük a kérést, máskülönben megvizsgáljuk, hogy be vagyunk-e jelentkezve. Ha a session-ben találunk bejelentkezett felhasználót, akkor továbbengedjük a kérést, máskülönben a login-ra irányítjuk.

Ez egy komplexebb feladat volt, de lépésenként szépen fel lehetett építeni az alkalmazás megfelelő működését. A megoldás megtalálható a pub-on 01-contacts-user mappában.

Profil hozzáadása

Feladat

Bővítsük az alkalmazást egy profil oldallal, ahol megadhatunk profilképet, leírást is. Az oldalon lehessen updatelni a felhasználó tulajdonságait (jelszót nem kell).

Elsőként bővítsük a User osztályt a kép és a leírás opciókkal!

1
2
3
4
private StringProperty description = new SimpleStringProperty();
private StringProperty profilePic = new SimpleStringProperty();

// getters and setters

A UserDAO-t módosítsuk úgy, hogy az az update-re is képes legyen. Ehhez az addUser-t refaktoráljuk, hogy save-nek hívják!

1
User save(User user);

Az implementációt pedig a következőképpen módosítsuk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
private static final String INSERT_USER = "INSERT INTO USER (username, email, profilePic, description, password) VALUES (?,?,?,?,?)";
private static final String UPDATE_USER = "UPDATE User SET username=?, email=?, profilePic=?, description=? WHERE id=?";
...
@Override
public User save(User user) {

    try (Connection conn = DriverManager.getConnection(DB_CONN_STR);
            PreparedStatement pst = user.getId() <= 0 ? conn.prepareStatement(INSERT_USER, Statement.RETURN_GENERATED_KEYS) : conn.prepareStatement(UPDATE_USER)
    ) {
        pst.setString(1, user.getUsername());
        pst.setString(2, user.getEmail());
        pst.setString(3, user.getProfilePic());
        pst.setString(4, user.getDescription());

        if(user.getId() > 0) { // UPDATE
            pst.setInt(5, user.getId());
        }
        else{ // INSERT
            String hashedPwd = BCrypt.withDefaults().hashToString(12, user.getPassword().toCharArray());
            pst.setString(5, hashedPwd);
        }

        int affectedRows = pst.executeUpdate();
        if(affectedRows == 0){
            return null;
        }

        if(user.getId() <= 0){ // INSERT
            ResultSet genKeys = pst.getGeneratedKeys();
            if(genKeys.next()){
                user.setId(genKeys.getInt(1));
            }
        }

        user.setPassword(""); // do not return even the hashed password for the caller

    } catch (SQLException e) {
        e.printStackTrace();
    }

    return user;

}

Ahhoz, hogy megfelelően működjenek az adatbázis műveletek módosítanunk kell a DB sémát is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
create table USER
(
    id INTEGER not null
        constraint USER_pk
            primary key autoincrement,
    username text not null,
    password text not null,
    email text not null,
    profilePic text,
    description text
);

A regisztrálási oldalon most nem változtatunk, így nézzük a profil oldalt!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <jsp:include page="../WEB-INF/common/common-header.jsp"/>
    <script src="../js/profile.js" ></script>
    <title>Profile</title>
</head>
<body>
<jsp:include page="../WEB-INF/common/menu.jsp"/>


<div class="container bootstrap snippet">
    <div class="row">
        <div class="col-sm-10"><h1>${sessionScope.currentUser.username}</h1></div>
    </div>
    <div class="row">
        <div class="col-sm-3"><!--left col-->
            <div class="text-center">
                <c:choose>
                    <c:when test="${sessionScope.currentUser.profilePic.length() > 0}">
                        <img src="${sessionScope.currentUser.profilePic}" class="avatar img-circle img-thumbnail"
                            alt="avatar">
                    </c:when>
                    <c:otherwise>
                        <img src="http://ssl.gstatic.com/accounts/ui/avatar_2x.png" class="avatar img-circle img-thumbnail"
                            alt="avatar">
                    </c:otherwise>
                </c:choose>

                <h6>Upload a different photo...</h6>
                <input type="file" class="text-center center-block file-upload">
            </div>
            <br>

        </div><!--/col-3-->
        <div class="col-sm-9">
            <div class="tab-content">
                <div class="tab-pane active" id="home">
                    <hr>
                    <form class="form" action="../UserController" method="post">
                        <div class="form-group">

                            <div class="col-xs-6">
                                <label for="name"><h4>Name</h4></label>
                                <input type="text" class="form-control" name="name" id="name"
                                    placeholder="Name" value="${sessionScope.currentUser.username}">
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-xs-6">
                                <label for="email"><h4>Email</h4></label>
                                <input type="email" class="form-control" name="email" id="email"
                                    placeholder="your@email.com" value="${sessionScope.currentUser.email}">
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-xs-6">
                                <label for="description"><h4>Description</h4></label>
                                <input type="text" class="form-control" name="description" id="description"
                                    placeholder="a short description" value="${sessionScope.currentUser.description}">
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-xs-12">
                                <br>
                                <button class="btn btn-primary" type="submit"><i
                                        class="glyphicon glyphicon-ok-sign"></i> Save
                                </button>
                            </div>
                        </div>
                    </form>

                </div><!--/tab-pane-->
            </div><!--/tab-content-->

        </div><!--/col-9-->
    </div><!--/row-->

</div>
</body>
</html>

A fontosabb sorokat kiemeltük a könnyebbség végett. Mivel szeretnénk egy olyan működést, melynek során biztosítjuk, hogy új kép kiválasztása esetén a profilképet egyből meg is jelenítjük, így ezt jQuery segítségével oldjuk meg, melyet az első kiemelt sor húz be. A profil frissítésekor a UserController-hez küldjük a kérést, ahol az aktuálisan bejelentkezett felhasználó adatait módosítjuk (kivéve a jelszót).

Először lássuk a profile.js állományt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$(document).ready(function() {

    let readURL = function(input) {
        if (input.files && input.files[0]) {
            let reader = new FileReader();

            reader.onload = function (e) {
                $('.avatar').attr('src', e.target.result);
            }

            reader.readAsDataURL(input.files[0]);
        }
    }

    $(".file-upload").on('change', function(){
        readURL(this);
    });

    $(".form").submit( function(eventObj) {
        $("<input />").attr("type", "hidden")
            .attr("name", "profilePic")
            .attr("value", $('.avatar').attr('src') )
            .appendTo(".form");
        return true;
    });
});

Minden esetben, amikor változik a kiválasztott kép, akkor beolvassuk a képet egy FileReader segítségével, majd ezt állítjuk be a profilkép src attribútumának. Amikor a form-ot submit-eljük, akkor egy rejtett input field-ben adjuk át a profilePic nevű attribútumot a formData-ban, hogy ezt könnyen feldolgozhassuk a szerver oldalon.

Az elküldött információk feldolgozását a UserController doPost metódusa végzi:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    User user = (User) req.getSession().getAttribute("currentUser");

    if(req.getParameter("name") != null && !req.getParameter("name").isEmpty()){
        user.setUsername(req.getParameter("name"));
    }

    if(req.getParameter("email") != null && !req.getParameter("email").isEmpty()){
        user.setEmail(req.getParameter("email"));
    }

    if(req.getParameter("profilePic") != null && !req.getParameter("profilePic").isEmpty()){
        user.setProfilePic(req.getParameter("profilePic"));
    }

    if(req.getParameter("description") != null && !req.getParameter("description").isEmpty()){
        user.setDescription(req.getParameter("description"));
    }

    user = userDAO.save(user);

    req.getSession().setAttribute("currentUser", user);
    resp.sendRedirect("pages/profile.jsp");
}

Elsőként a session-ből kiszedjük az aktuális felhasználót, majd frissítjük az attribútumait és elmentjük az adatbázisban, majd frissítjük a sessionben lévő currentUser objektumot.

Miután ezekkel megvagyunk, helyezzük el a menüben is a profil menüpontot:

1
2
3
4
5
6
...
<div class='dropdown-menu dropdown-menu-right' aria-labelledby='navbarDropdownMenuLink'>
    <a class='dropdown-item' href='../pages/profile.jsp'>Profile</a>
    <a class='dropdown-item' href='../LogoutController'>Logout</a>
</div>
...

Törlés és módosítás

Feladat

Bővítsük az alkalmazást törlés és módosítás elemekkel

Elsőként helyezzük el a törlés és módosítás ikonokat a listában! Ezekhez az ikonokhoz font-awesome-t használunk, így azt a common-header.jsp-ben be kell húznunk!

common-header.jsp:

1
2
3
...
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
...

Ezután a list-contact.jsp oldalon adjunk hozzá egy új oszlopot, melyben ezek az ikonok lesznek megjelenítve:

1
2
3
4
5
6
7
8
...
<th scope="col">Actions</th>
...
<td>
    <a href="../UpdateContactController?contactId=${item.id}"><i class="fas fa-edit"></i></a>
    <a href="../DeleteContactController?contactId=${item.id}"><i class="fas fa-trash"></i></a>
</td>
...

Kezdjünk az UpdateContactController megvalósításával:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@WebServlet("/UpdateContactController")
public class UpdateContactController extends HttpServlet {
    private ContactDAO dao = new ContactDAOImpl();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String contactIdStr = req.getParameter("contactId");

        if(contactIdStr != null && !contactIdStr.isEmpty()){
            int contactId = Integer.parseInt(contactIdStr);
            Contact contact = dao.findById(contactId);
            req.setAttribute("contact", contact);
        }

        req.getRequestDispatcher("pages/add-contact.jsp").forward(req, resp);
    }
}

Maga az update nem egy bonyolult konstrukció. A queryString-ben elküldött paraméter alapján lekérjük a megfelelő kontaktot, majd ezt a kérésben, mint attribútum elhelyezzük. Ezek után a a kérést továbbítjuk a add-contact.jsp oldalnak (mely ebben a formában lehet, hogy megérett egy átnevezésre, de most ezzel nem foglalkozunk). Itt pedig ki fogjuk olvasni a kérésben elhelyezett attribútum összes értékét és ezzel töltjük fel a felületi vezérlőket.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
<%@ page import="hu.alkfejl.model.Phone" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

<html>
<head>
    <jsp:include page="../WEB-INF/common/common-header.jsp"/>
    <script src="${pageContext.request.contextPath}/js/add-contact.js"></script>
    <title>Add Contact</title>
</head>
<body>
<jsp:include page="../WEB-INF/common/menu.jsp"/>
<jsp:useBean id="contact" class="hu.alkfejl.model.Contact" scope="request" />

<div class="container">
    <form action="${pageContext.request.contextPath}/ContactController" method="post">
        <input type="hidden" name="id" value="${contact.id}" />
        <div class="form-group">
            <label for="name">Name</label>
            <input required name="name" type="text" class="form-control" id="name"
                   placeholder="Enter name" value="${contact.name}"/>
        </div>
        <div class="form-group">
            <label for="email">Email</label>
            <input required name="email" type="email" class="form-control" id="email"
                   placeholder="Email" value="${contact.email}"/>
        </div>
        <div class="form-group">
            <label for="dateOfBirth">Date of Birth</label>
            <input required id="dateOfBirth" name="dateOfBirth" type="date" class="form-control"
                   placeholder="Date of Birth" value="${contact.dateOfBirth}"/>
        </div>
        <div class="form-group">
            <label>Phones</label>
            <c:if test="${contact.phones.size() > 0}">
                <c:forEach var="phone" items="${contact.phones}">
                    <div class="row">
                        <div class="col">
                            <input name="phoneValues" type="text" class="form-control mb-3"
                                   placeholder="Enter phone number" value="${phone.number}"/>
                        </div>
                        <div class="col">
                            <select name="phoneTypes" class="custom-select">
                                <c:forEach var="phoneType" items="<%=Phone.PhoneType.values()%>">
                                    <c:if test="${phone.phoneType.value.equals(phoneType.value)}">
                                        <option selected="true" value="${phoneType.value}">${phoneType.value}</option>
                                    </c:if>
                                    <c:if test="${!phone.phoneType.value.equals(phoneType.value)}">
                                        <option value="${phoneType.value}">${phoneType.value}</option>
                                    </c:if>
                                </c:forEach>
                            </select>
                        </div>
                        <div class="col md-2">
                            <c:if test="${phone.equals(contact.phones.get(contact.phones.size() - 1))}">
                                <button type="button" class="btn btn-secondary" onclick="newRow(this)">New</button>
                            </c:if>
                            <c:if test="${!phone.equals(contact.phones.get(contact.phones.size() - 1))}">
                                <button type="button" class="btn btn-secondary" onclick="deleteRow(this)">Delete</button>
                            </c:if>
                        </div>
                    </div>
                </c:forEach>
            </c:if>
            <c:if test="${contact.phones == null or contact.phones.size() == 0}">
                <div class="row">
                    <div class="col">
                        <input name="phoneValues" type="text" class="form-control mb-3"
                               placeholder="Enter phone number"/>
                    </div>
                    <div class="col">
                        <select name="phoneTypes" class="custom-select">
                            <c:forEach var="phoneType" items="<%=Phone.PhoneType.values()%>">
                                <option value="${phoneType.value}">${phoneType.value}</option>
                            </c:forEach>
                        </select>
                    </div>
                    <div class="col md-2">
                        <button onclick="newRow(this)" type="button" class="btn btn-secondary">New</button>
                    </div>
                </div>
            </c:if>
        </div>
        <div class="form-group">
            <label for="address">Address</label>
            <input id="address" name="address" type="text" class="form-control" id="address"
                   placeholder="Address" value="${contact.address}"/>
        </div>
        <div class="form-group">
            <label for="company">Company</label>
            <input id="company" name="company" type="text" class="form-control" id="company"
                   placeholder="Company" value="${contact.company}"/>
        </div>
        <div class="form-group">
            <label for="address">Position</label>
            <input id="position" name="position" type="text" class="form-control" id="position"
                   placeholder="Position" value="${contact.position}"/>
        </div>
        <button id="submit" type="submit" class="btn btn-primary">Submit</button>
    </form>
</div>
</body>
</html>

A fenti kódrészletben kiemeltük a fontosabb változtatásokat. Először is a forward miatt az add-contact.js elérése nem biztos lesz megfelelő, így a pageContext.request.contextPath URL-t használjuk, mely megadja a context útvonalát (pl.: http://localhost:8080/contacts_web_war). Ezután az előzetesen a request-ben elhelyezett contact attribútumot kiolvassuk a useBean segítségével, majd ezt fogjuk felhasználni az oldalon, hogy feltöltsük a field-ek értékeit.

A meglévő input field-ek mellett hozzáadunk egy rejtett mezőt az id számára is, mivel err a feldolgozás során szükségünk lesz, hiszen frissíteni szeretnénk. Ezen felül az összes field-hez megadjuk a value="${contact.property} attribútumot, hogy ki is töltse az input-okat az aktuális értékekkel.

A telefonszámok esetében azonban összetettebb feladatunk van és némi átszervezést is igényel a feladat. Amennyiben a kontakthoz tartoznak telefonszámok, akkor azokat ki kell írnunk, illetve mindegyik mellett a Delete opciót adjuk meg, kivéve az utolsónál, ahol a New opciót. Az első c:if, akkor fut le, amikor vannak telefonszámok. Ebben az esetben végigiterálunk a már megadott telefonszámokon és mindegyikhez megadunk egy div-et (row class-al). Kiírjuk magát a telefonszámot (40. sor) egy vezérlőben, majd egy lenyílóba belerakjuk az összes phoneType opciót és közben figyeljük, hogy ha az adott phoneType megegyezik azzal, ami ki volt választva, akkor rákerül a selected attribútum is, azaz ő lesz kiválasztva a felületen.

A gombok megadásakor (54-61. sor) figyeljük, hogy az utolsó elemnél vagyunk-e. Az utolsó elemnél a New szöveg lesz a gombon, illetve a newRow(this) eseménykezelőt adjuk meg. Ellenkező esetben a Delete szöveget alkalmazzuk és a deleteRow(this) eseménykezelőt. A newRow és a deleteRow függvényeket az add-contact.js-ben adjuk meg, melyet szintén módosítanunk kellett a kontakt frissítés érdekében. A 65-82. sorig tartó részben szerepel az, amikor nincs megadva telefonszám a kontakhoz, amely alapján csak egy üres beviteli sort adunk meg.

Ezután lássuk a módosított JavaScript részt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function newRow(button){
   button = $(button);
   let newRowToAdd = button.closest('.row').clone(true);
   newRowToAdd.find('input').val('');

   button.html('Delete');
   button.attr('onclick', 'deleteRow(this)');
   let phoneRows = $('.row');
   phoneRows.last().parent().append(newRowToAdd);
}

function deleteRow(button){
   button.closest('.row').remove();
}

A JS kód funkcionalitása lényegében megmaradt, csupán a kiszerveztük azokat két külön függvénybe.

A fentiek eddig a pontig teljesen jól működnek, azonban a kontakt frissítésekor egy új rekord fog megjelenni, mivel a ContactController-ben is hozzá kell igazítanunk a működést a most megvalósított elemekhez.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setCharacterEncoding("utf-8");
    response.setCharacterEncoding("utf-8");

    int contactId = 0;
    try {
        contactId = Integer.parseInt(request.getParameter("id"));
    } catch (NumberFormatException ex){
        ex.printStackTrace();
    }

    Contact c = dao.findById(contactId);

    // check if c exists -> if no then construct a new one (it's a save)
    if(c == null){
        c = new Contact();
    }

    try {
        c.setName(request.getParameter("name"));
        c.setEmail(request.getParameter("email"));
        c.setDateOfBirth(LocalDate.parse(request.getParameter("dateOfBirth")));
        c.setAddress(request.getParameter("address"));
        c.setCompany(request.getParameter("company"));
        c.setPosition(request.getParameter("position"));

        User currentUser = (User) request.getSession().getAttribute("currentUser");
        c.setUser(currentUser);

        String[] phoneValues = request.getParameterValues("phoneValues");
        String[] phoneTypes = request.getParameterValues("phoneTypes");
        String[] phoneIds = request.getParameterValues("phoneIds");

        List<Phone> phones = new ArrayList<>();
        for(int i = 0; i < phoneValues.length; i++){
            Phone p = null;
            if(phoneIds != null && phoneIds[i] != null && !phoneIds[i].isEmpty()){
                p = phoneDAO.findById(Integer.parseInt(phoneIds[i]));
            }

            if(p == null){
                p = new Phone();
            }

            p.setNumber(phoneValues[i]);
            final String phoneTypeString = phoneTypes[i];
            Optional<Phone.PhoneType> foundPhoneType = Arrays.stream(Phone.PhoneType.values()).filter(phoneType -> phoneType.getValue().equals(phoneTypeString)).findFirst();
            p.setPhoneType(foundPhoneType.orElse(Phone.PhoneType.UNKNOWN));
            phones.add(p);
        }
        c.setPhones(phones);
        dao.save(c);


        response.sendRedirect("pages/list-contact.jsp");

    } catch (Exception e) {
        e.printStackTrace();
    }

}

A legfontosabb az id kiolvasása, mely alapján lekérdezzük a meglévő kontaktot. Később a save hatására mentés vagy beillesztés is történhet, így ezt nem kell majd igazítanunk. Ezután feldolgozzuk a telefonszámokat, azonban szükségünk lesz a meglévő id-kra, melyeket a phoneIds-ban küldünk el, így ezt a jsp-ben is el kell helyeznünk egy hidden field-be:

1
<input type="hidden" name="phoneIds" value="${phone.id}" />

Miután kiolvastuk a megfelelő telefonokat (Phone objektumok létrehozása) elmentjük a kontaktot. Ezen a ponton egy nagyobb refaktorálást is tegyünk meg, amelyet kicsit halogattunk. Ennek eredményeképpen a DAO réteget átalakítottuk, hogy a egy kontakt mentése esetén a hozzá tartozó telefonszámokat is frissítse az adatelérési réteg. A módosításokat ezen a ponton nem fogjuk kifejteni, a módosított DAO elérhető a pub-ban.

Az update mellett a delete-hez tartozó servlet a következőképpen néz ki:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@WebServlet("/DeleteContactController")
public class DeleteContactController extends HttpServlet {

    ContactDAO dao = ContactDAOImpl.getInstance();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            int contactId = Integer.parseInt(req.getParameter("contactId"));
            dao.delete(contactId);
        }
        catch (NumberFormatException ex){
            ex.printStackTrace();
        }

        resp.sendRedirect("pages/list-contact.jsp");
    }
}

Itt is érdemes megjegyezni, hogy a kontakt kitörlése az összes hozzátartozó telefonszámot is kitörli. További feladatként hozzáadhatunk egy megerősítést a törléshez, de ezen a ponton ezzel most nem foglalkozunk.


Utolsó frissítés: 2024-03-19 07:21:59