diff --git a/emmas/pom.xml b/emmas/pom.xml index 81ea17b..485d1f6 100644 --- a/emmas/pom.xml +++ b/emmas/pom.xml @@ -80,10 +80,23 @@ nz.ac.waikato.cms.weka - weka-stable - 3.8.6 + weka-dev + 3.9.6 + + nz.ac.waikato.cms.weka + SMOTE + 1.0.3 + + + + nz.ac.waikato.cms.weka + naiveBayesTree + 1.0.2 + + + jakarta.activation @@ -114,6 +127,11 @@ 4.13.1 test + + org.mindrot + jbcrypt + 0.4 + diff --git a/emmas/src/main/java/dev/brianweloba/dao/EventDAO.java b/emmas/src/main/java/dev/brianweloba/dao/EventDAO.java index 2713063..5ba07f5 100644 --- a/emmas/src/main/java/dev/brianweloba/dao/EventDAO.java +++ b/emmas/src/main/java/dev/brianweloba/dao/EventDAO.java @@ -1,13 +1,13 @@ package dev.brianweloba.dao; -import java.util.List; - import dev.brianweloba.model.Event; import dev.brianweloba.util.HibernateUtil; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import jakarta.transaction.Transactional; +import java.util.List; + public class EventDAO { @Transactional @@ -28,7 +28,7 @@ public Event create(Event event) { } } - public int countAll(){ + public int countAll() { try (EntityManager manager = HibernateUtil.getEntityManager()) { return manager.createQuery("SELECT COUNT(e) FROM Event e", Long.class) .getSingleResult() @@ -36,7 +36,7 @@ public int countAll(){ } } - public List findPaginated(int startIndex, int pageSize){ + public List findPaginated(int startIndex, int pageSize) { try (EntityManager manager = HibernateUtil.getEntityManager()) { return manager.createQuery("SELECT e FROM Event e ORDER BY e.eventDate", Event.class) .setFirstResult(startIndex) @@ -48,6 +48,7 @@ public List findPaginated(int startIndex, int pageSize){ public List search(String query) { try (EntityManager manager = HibernateUtil.getEntityManager()) { String searchTerm = "%" + query.toLowerCase() + "%"; + return manager.createQuery( "SELECT e FROM Event e LEFT JOIN FETCH e.rsvps WHERE " + " LOWER(e.title) LIKE :query OR" + @@ -66,7 +67,7 @@ public Event findById(Long id) { } } - public Event findByIdAndToken(Long id,String token){ + public Event findByIdAndToken(Long id, String token) { EntityManager manager = HibernateUtil.getEntityManager(); try { TypedQuery query = manager.createQuery( @@ -78,36 +79,37 @@ public Event findByIdAndToken(Long id,String token){ System.out.println("Id: " + id); System.out.println("Query: " + query); return query.getSingleResult(); - }catch (Exception e) { + } catch (Exception e) { if (manager.getTransaction().isActive()) { manager.getTransaction().rollback(); } throw e; - }finally { + } finally { manager.close(); } } - public List findAll() { + public List findAll() { try (EntityManager manager = HibernateUtil.getEntityManager()) { - TypedQuery query = manager.createQuery( + return manager.createQuery( "SELECT DISTINCT e FROM Event e LEFT JOIN FETCH e.rsvps ORDER BY e.eventDate", Event.class - ); - return query.getResultList(); + ).getResultList(); } } + public void update(Event event, String token) { EntityManager manager = HibernateUtil.getEntityManager(); try { - Event eventToUpdate = findByIdAndToken(event.getId(),token); - if(eventToUpdate == null){ + Event eventToUpdate = findByIdAndToken(event.getId(), token); + if (eventToUpdate == null) { return; } manager.getTransaction().begin(); eventToUpdate.setTitle(event.getTitle()); + eventToUpdate.setEventHost(event.getEventHost()); eventToUpdate.setEventType(event.getEventType()); eventToUpdate.setEventLocation(event.getEventLocation()); eventToUpdate.setDescription(event.getDescription()); @@ -126,15 +128,25 @@ public void update(Event event, String token) { } } - public void delete(Long id) { + public boolean delete(Long id, String token) { EntityManager manager = HibernateUtil.getEntityManager(); try { manager.getTransaction().begin(); - Event event = manager.find(Event.class, id); - if (event != null) { - manager.remove(event); + Event event = manager.createQuery( + "SELECT e FROM Event e WHERE e.id = :id AND e.editToken = :token", Event.class) + .setParameter("id", id) + .setParameter("token", token) + .getResultList() + .stream() + .findFirst() + .orElse(null); + + if (event == null) { + return false; } + manager.remove(event); manager.getTransaction().commit(); + return true; } catch (Exception e) { if (manager.getTransaction().isActive()) { manager.getTransaction().rollback(); diff --git a/emmas/src/main/java/dev/brianweloba/dao/RsvpDAO.java b/emmas/src/main/java/dev/brianweloba/dao/RsvpDAO.java index 8f8989e..38c5799 100644 --- a/emmas/src/main/java/dev/brianweloba/dao/RsvpDAO.java +++ b/emmas/src/main/java/dev/brianweloba/dao/RsvpDAO.java @@ -4,9 +4,12 @@ import dev.brianweloba.model.RSVP; import dev.brianweloba.util.HibernateUtil; import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; + +import java.util.List; public class RsvpDAO { - public RSVP create(RSVP rsvp,Long eventId) { + public void create(RSVP rsvp, Long eventId) { EntityManager manager = HibernateUtil.getEntityManager(); try { manager.getTransaction().begin(); @@ -16,23 +19,23 @@ public RSVP create(RSVP rsvp,Long eventId) { throw new IllegalArgumentException("Event not found"); } - Long currentGuests = manager.createQuery( - "SELECT SUM(r.guests) FROM RSVP r WHERE r.event = :event", Long.class) - .setParameter("event", event) - .getSingleResult(); + boolean alreadyExists = checkExistingRsvp(manager, event, rsvp.getEmail()); + if (alreadyExists) { + throw new IllegalArgumentException("Email already registered for this event"); + } - currentGuests = currentGuests == null ? 0 : currentGuests; + Long currentGuests = getCurrentGuestCount(manager, event); - int guestCount = rsvp.getGuests(); - if (currentGuests + guestCount > event.getEventCapacity()) { + if (currentGuests + rsvp.getGuests() > event.getEventCapacity()) { throw new IllegalArgumentException("Event has reached capacity"); } rsvp.setEvent(event); + event.getRsvps().add(rsvp); manager.persist(rsvp); manager.getTransaction().commit(); - return rsvp; + } catch (Exception e) { if (manager.getTransaction().isActive()) { manager.getTransaction().rollback(); @@ -42,4 +45,50 @@ public RSVP create(RSVP rsvp,Long eventId) { manager.close(); } } + + private boolean checkExistingRsvp(EntityManager manager, Event event, String email) { + try { + return manager.createQuery( + "SELECT COUNT(r) > 0 FROM RSVP r " + + "WHERE r.event = :event AND LOWER(r.email) = LOWER(:email)", + Boolean.class) + .setParameter("event", event) + .setParameter("email", email) + .getSingleResult(); + } catch (NoResultException e) { + return false; + } + } + + private Long getCurrentGuestCount(EntityManager manager, Event event) { + try { + Long count = manager.createQuery( + "SELECT SUM(r.guests) FROM RSVP r WHERE r.event = :event", + Long.class) + .setParameter("event", event) + .getSingleResult(); + return count != null ? count : 0L; + } catch (NoResultException e) { + return 0L; + } + } + + public List getEventAttendees(Long eventId) { + try (EntityManager manager = HibernateUtil.getEntityManager()) { + return manager.createQuery("SELECT r FROM RSVP r WHERE r.event.id = :eventId", RSVP.class) + .setParameter("eventId", eventId) + .getResultList(); + } + } + + // Additional useful method + public Event findEventWithRsvps(Long eventId) { + try (EntityManager manager = HibernateUtil.getEntityManager()) { + return manager.createQuery( + "SELECT e FROM Event e LEFT JOIN FETCH e.rsvps WHERE e.id = :id", + Event.class) + .setParameter("id", eventId) + .getSingleResult(); + } + } } \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/dao/UserDAO.java b/emmas/src/main/java/dev/brianweloba/dao/UserDAO.java new file mode 100644 index 0000000..3d73c50 --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/dao/UserDAO.java @@ -0,0 +1,44 @@ +package dev.brianweloba.dao; + +import dev.brianweloba.model.User; +import dev.brianweloba.util.HibernateUtil; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; + +public class UserDAO { + public User findByUsername(String username) { + try (EntityManager manager = HibernateUtil.getEntityManager()) { + return manager.createQuery("SELECT u FROM User u WHERE u.username = :username", User.class) + .setParameter("username", username) + .getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + public User findByEmail(String email) { + try (EntityManager manager = HibernateUtil.getEntityManager()) { + return manager.createQuery("SELECT u FROM User u WHERE u.email = :email", User.class) + .setParameter("email", email) + .getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + public void save(User user) { + EntityManager manager = HibernateUtil.getEntityManager(); + try { + manager.getTransaction().begin(); + manager.persist(user); + manager.getTransaction().commit(); + } catch (Exception e) { + if (manager.getTransaction().isActive()) { + manager.getTransaction().rollback(); + } + throw e; + } finally { + manager.close(); + } + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/filter/AuthFilter.java b/emmas/src/main/java/dev/brianweloba/filter/AuthFilter.java new file mode 100644 index 0000000..419e2e5 --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/filter/AuthFilter.java @@ -0,0 +1,27 @@ +package dev.brianweloba.filter; + +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebFilter("/events/*") +public class AuthFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + HttpSession session = httpRequest.getSession(false); + + if (session == null || session.getAttribute("user") == null) { + httpResponse.sendRedirect(httpRequest.getContextPath() + "/login"); + } else { + chain.doFilter(request, response); + } + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/model/Event.java b/emmas/src/main/java/dev/brianweloba/model/Event.java index 8915a35..e3d7e99 100644 --- a/emmas/src/main/java/dev/brianweloba/model/Event.java +++ b/emmas/src/main/java/dev/brianweloba/model/Event.java @@ -2,15 +2,17 @@ import jakarta.persistence.*; import jakarta.validation.constraints.*; -import lombok.Data; +import lombok.*; import java.util.Date; +import java.util.HashSet; import java.util.Set; import java.util.UUID; @Entity @Table(name = "events") -@Data +@Getter @Setter @ToString(exclude = "rsvps") +@NoArgsConstructor public class Event { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -52,8 +54,8 @@ public class Event { @Column(name = "event_capacity") private int eventCapacity; - @OneToMany(mappedBy = "event",fetch = FetchType.LAZY, cascade = CascadeType.ALL) - private Set rsvps; + @OneToMany(mappedBy = "event", cascade = CascadeType.ALL,orphanRemoval = true) + private Set rsvps = new HashSet<>(); @Column(nullable = false, unique = true, length = 36, name = "edit_token") private String editToken; @@ -64,4 +66,26 @@ public void generateEditToken() { this.editToken = UUID.randomUUID().toString(); } } + + public void addRsvp(RSVP rsvp) { + rsvps.add(rsvp); + rsvp.setEvent(this); + } + + public void removeRsvp(RSVP rsvp) { + rsvps.remove(rsvp); + rsvp.setEvent(null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Event)) return false; + return id != null && id.equals(((Event) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } } diff --git a/emmas/src/main/java/dev/brianweloba/model/RSVP.java b/emmas/src/main/java/dev/brianweloba/model/RSVP.java index c579b49..7832f28 100644 --- a/emmas/src/main/java/dev/brianweloba/model/RSVP.java +++ b/emmas/src/main/java/dev/brianweloba/model/RSVP.java @@ -2,13 +2,16 @@ import jakarta.persistence.*; -import lombok.Data; +import lombok.*; import java.util.Date; -@Data @Entity @Table(name = "rsvps") +@Getter +@Setter +@ToString +@NoArgsConstructor public class RSVP { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -18,6 +21,10 @@ public class RSVP { @JoinColumn(name = "event_id", nullable = false) private Event event; + @ManyToOne + @JoinColumn(name = "user_id", nullable = true) + private User user; + private String name; private String email; private int guests; @@ -26,4 +33,16 @@ public class RSVP { private Date createdAt; @Column(name = "updated_at", insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") private Date updatedAt; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RSVP)) return false; + return id != null && id.equals(((RSVP) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } } diff --git a/emmas/src/main/java/dev/brianweloba/model/User.java b/emmas/src/main/java/dev/brianweloba/model/User.java new file mode 100644 index 0000000..cbe676d --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/model/User.java @@ -0,0 +1,63 @@ +package dev.brianweloba.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List rsvps = new ArrayList<>(); + + @NotBlank(message = "Username is required") + @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") + @Column(nullable = false, unique = true) + private String username; + + @NotBlank(message = "Password is required") + @Size(min = 8, message = "Password must be at least 8 characters long") + @Column(nullable = false) + private String password; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + @Column(nullable = false, unique = true) + private String email; + + @Column(name = "created_at", insertable = false, updatable = false, + columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + private Date createdAt; + + @Column(name = "updated_at", insertable = false, updatable = false, + columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + private Date updatedAt; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User)) return false; + return id != null && id.equals(((User) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} + diff --git a/emmas/src/main/java/dev/brianweloba/servlet/CreateEvent.java b/emmas/src/main/java/dev/brianweloba/servlet/CreateEvent.java index b7d3530..2eaabfc 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/CreateEvent.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/CreateEvent.java @@ -2,6 +2,7 @@ import dev.brianweloba.dao.EventDAO; import dev.brianweloba.model.Event; +import dev.brianweloba.util.CookieUtil; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.*; @@ -39,13 +40,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) event = eventDAO.create(event); session.removeAttribute("event"); - - Cookie tokenCookie = new Cookie("event_" + event.getId() + "_token", event.getEditToken()); - tokenCookie.setMaxAge(60 * 60 * 24 * 365); - tokenCookie.setPath("/"); - response.addCookie(tokenCookie); - + CookieUtil.setCookie(response, event); response.sendRedirect(request.getContextPath() + "/events"); } + + } diff --git a/emmas/src/main/java/dev/brianweloba/servlet/DeleteEvent.java b/emmas/src/main/java/dev/brianweloba/servlet/DeleteEvent.java index cb9d9f9..f44f040 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/DeleteEvent.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/DeleteEvent.java @@ -1,4 +1,63 @@ package dev.brianweloba.servlet; -public class DeleteEvent { -} +import dev.brianweloba.dao.EventDAO; +import dev.brianweloba.model.Event; +import dev.brianweloba.util.CookieUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/events/delete") +public class DeleteEvent extends HttpServlet { + private final EventDAO eventDAO = new EventDAO(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + try { + String idParam = request.getParameter("id"); + if (idParam == null || idParam.trim().isEmpty()) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Event ID is required"); + return; + } + + Long eventId = Long.parseLong(idParam); + + String token = CookieUtil.getEditTokenFromCookies(request, eventId); + if (token == null) { + request.setAttribute("error", "Invalid token. You don't have permission to delete this event."); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + return; + } + + Event event = eventDAO.findByIdAndToken(eventId, token); + if (event == null) { + request.setAttribute("error", "Event not found or you don't have permission to delete this event."); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + return; + } + + boolean success = eventDAO.delete(eventId, token); + + if (success) { + request.getSession().setAttribute("statusMessage", "Event deleted successfully!"); + response.sendRedirect(request.getContextPath() + "/events"); + } else { + request.setAttribute("error", "Failed to delete event."); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + } + + } catch (NumberFormatException e) { + request.setAttribute("error", "Invalid event ID format"); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + } catch (Exception e) { + request.setAttribute("error", "Error deleting event: " + e.getMessage()); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + } + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/servlet/EventServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/EventServlet.java index ac6e0e2..464b33e 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/EventServlet.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/EventServlet.java @@ -2,6 +2,7 @@ import dev.brianweloba.dao.EventDAO; import dev.brianweloba.model.Event; +import dev.brianweloba.util.CookieUtil; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; @@ -9,7 +10,10 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; + @WebServlet("/events") public class EventServlet extends HttpServlet { @@ -38,6 +42,15 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) List events = eventDAO.findPaginated(startIndex, pageSize); + Map editTokens = new HashMap<>(); + for (Event event : events) { + String token = CookieUtil.getEditTokenFromCookies(request, event.getId()); + if (token != null) { + editTokens.put(event.getId(), token); + } + } + request.setAttribute("editTokens", editTokens); + request.setAttribute("events", events); request.setAttribute("currentPage", currentPage); request.setAttribute("pageSize", pageSize); diff --git a/emmas/src/main/java/dev/brianweloba/servlet/IndexServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/IndexServlet.java index 18cba42..0af7713 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/IndexServlet.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/IndexServlet.java @@ -7,11 +7,15 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; -@WebServlet("/index") +@WebServlet( "/index") public class IndexServlet extends HttpServlet { @@ -20,8 +24,20 @@ public class IndexServlet @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - List events = eventDAO.findAll(); - request.setAttribute("events", events); - request.getRequestDispatcher("/WEB-INF/views/index.jsp").forward(request, response); + HttpSession session = request.getSession(false); + if (session != null && session.getAttribute("user") != null) { + List events = eventDAO.findAll(); + + Set eventTypes = events.stream() + .map(Event::getEventType) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + request.setAttribute("events", events); + request.setAttribute("eventTypes", eventTypes); + request.getRequestDispatcher("/WEB-INF/views/index.jsp").forward(request, response); + } else { + response.sendRedirect(request.getContextPath() + "/login"); + } } } diff --git a/emmas/src/main/java/dev/brianweloba/servlet/LoginServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/LoginServlet.java new file mode 100644 index 0000000..59552a6 --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/servlet/LoginServlet.java @@ -0,0 +1,43 @@ +package dev.brianweloba.servlet; + +import dev.brianweloba.dao.UserDAO; +import dev.brianweloba.model.User; +import dev.brianweloba.util.PasswordUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebServlet("/login") +public class LoginServlet extends HttpServlet { + private final UserDAO userDAO = new UserDAO(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + User user = userDAO.findByUsername(username); + + if (user != null && PasswordUtil.checkPassword(password, user.getPassword())) { + HttpSession session = request.getSession(); + session.setAttribute("user", user); + + response.sendRedirect(request.getContextPath() + "/index"); + } else { + request.setAttribute("error", "Invalid username or password"); + request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response); + } + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/servlet/LogoutServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/LogoutServlet.java new file mode 100644 index 0000000..50c49ec --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/servlet/LogoutServlet.java @@ -0,0 +1,23 @@ +package dev.brianweloba.servlet; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +@WebServlet("/logout") +public class LogoutServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + response.sendRedirect(request.getContextPath() + "/index"); + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/servlet/RSVPServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/RSVPServlet.java index ccf4678..401f9c9 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/RSVPServlet.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/RSVPServlet.java @@ -1,19 +1,62 @@ package dev.brianweloba.servlet; import com.google.gson.JsonObject; +import dev.brianweloba.dao.EventDAO; import dev.brianweloba.dao.RsvpDAO; import dev.brianweloba.model.RSVP; +import dev.brianweloba.model.User; +import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import java.io.IOException; +import java.util.List; @WebServlet("/events/rsvp") public class RSVPServlet extends HttpServlet { private final RsvpDAO rsvpDAO = new RsvpDAO(); + private final EventDAO eventDAO = new EventDAO(); + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException { + try{ + String eventIdParam = request.getParameter("eventId"); + if(eventIdParam == null || eventIdParam.trim().isEmpty()){ + request.setAttribute("error", "Event ID is required"); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + return; + } + long eventId; + try { + eventId = Long.parseLong(eventIdParam); + } catch (NumberFormatException e) { + request.setAttribute("error", "Invalid Event ID format"); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + return; + } + + List attendees = rsvpDAO.getEventAttendees(eventId); + + int totalGuests = 0; + for (RSVP rsvp : attendees) { + totalGuests += rsvp.getGuests(); + } + + request.setAttribute("attendees", attendees); + request.setAttribute("totalGuests", totalGuests); + + request.getRequestDispatcher("/WEB-INF/views/attendees.jsp").forward(request, response); + + } catch (Exception e) { + request.setAttribute("error", "Error fetching attendees: " + e.getMessage()); + request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); + } + + + } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("application/json"); @@ -21,7 +64,6 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) try { RSVP rsvp = new RSVP(); - rsvp.setName(request.getParameter("name")); rsvp.setEmail(request.getParameter("email")); rsvp.setGuests(Integer.parseInt(request.getParameter("guests"))); @@ -36,6 +78,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) throw new IllegalArgumentException("Guests must be a positive number"); } + HttpSession session = request.getSession(); + User user = (User) session.getAttribute("user"); + if (user != null) { + rsvp.setUser(user); + } + rsvpDAO.create(rsvp, Long.parseLong(request.getParameter("eventId"))); jsonResponse.addProperty("success", true); diff --git a/emmas/src/main/java/dev/brianweloba/servlet/RegistrationServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/RegistrationServlet.java new file mode 100644 index 0000000..001bd82 --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/servlet/RegistrationServlet.java @@ -0,0 +1,59 @@ +package dev.brianweloba.servlet; + +import dev.brianweloba.dao.UserDAO; +import dev.brianweloba.model.User; +import dev.brianweloba.util.PasswordUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet("/register") +public class RegistrationServlet extends HttpServlet { + private final UserDAO userDAO = new UserDAO(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.getRequestDispatcher("/WEB-INF/views/register.jsp").forward(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String username = request.getParameter("username"); + String email = request.getParameter("email"); + String password = request.getParameter("password"); + String confirmPassword = request.getParameter("confirmPassword"); + + if (!password.equals(confirmPassword)) { + request.setAttribute("error", "Passwords do not match"); + request.getRequestDispatcher("/WEB-INF/views/register.jsp").forward(request, response); + return; + } + + if (userDAO.findByUsername(username) != null) { + request.setAttribute("error", "Username already taken"); + request.getRequestDispatcher("/WEB-INF/views/register.jsp").forward(request, response); + return; + } + + if (userDAO.findByEmail(email) != null) { + request.setAttribute("error", "Email already registered"); + request.getRequestDispatcher("/WEB-INF/views/register.jsp").forward(request, response); + return; + } + + User user = new User(); + user.setUsername(username); + user.setEmail(email); + user.setPassword(PasswordUtil.hashPassword(password)); + + userDAO.save(user); + + response.sendRedirect(request.getContextPath() + "/login"); + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/servlet/SearchServlet.java b/emmas/src/main/java/dev/brianweloba/servlet/SearchServlet.java index fe8426a..4468c5a 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/SearchServlet.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/SearchServlet.java @@ -23,17 +23,16 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t System.out.println("query = " + query); if(query !=null && !query.trim().isEmpty()){ events = eventDAO.search(query.trim()); - - }else { + } else { events = eventDAO.findAll(); - request.setAttribute("events", events); } + System.out.println("EVENT:"+events.getFirst()); + System.out.println("RSVPS:"+events.getFirst().getRsvps()); + request.setAttribute("events", events); request.setAttribute("searchQuery", query); request.getRequestDispatcher("/WEB-INF/views/eventsBody.jsp").forward(request, response); } - - } diff --git a/emmas/src/main/java/dev/brianweloba/servlet/UpdateEvent.java b/emmas/src/main/java/dev/brianweloba/servlet/UpdateEvent.java index d173657..cba5afe 100644 --- a/emmas/src/main/java/dev/brianweloba/servlet/UpdateEvent.java +++ b/emmas/src/main/java/dev/brianweloba/servlet/UpdateEvent.java @@ -3,10 +3,10 @@ import dev.brianweloba.dao.EventDAO; import dev.brianweloba.model.Event; import dev.brianweloba.services.EventService; +import dev.brianweloba.util.CookieUtil; import dev.brianweloba.util.ValidationUtil; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -22,7 +22,7 @@ public class UpdateEvent extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Long eventId = Long.parseLong(request.getParameter("id")); - String token = getEditTokenFromCookies(request, eventId); + String token = CookieUtil.getEditTokenFromCookies(request, eventId); Event event = getEvent(request, response, token, eventId); if (event == null) return; @@ -50,7 +50,7 @@ private Event getEvent(HttpServletRequest request, HttpServletResponse response, @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Long eventId = Long.parseLong(request.getParameter("eventId")); - String token = getEditTokenFromCookies(request, eventId); + String token = CookieUtil.getEditTokenFromCookies(request, eventId); Event event = getEvent(request, response, token, eventId); if (event == null) return; @@ -59,26 +59,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) if(ValidationUtil.validateEvent(request,response,event,"/WEB-INF/views/update.jsp")) return; + event.setEventType(request.getParameter("eventType").toUpperCase()); + + if(event.getEventType().isEmpty()){ + request.setAttribute("eventTypeError", "Event Type is empty"); + request.getRequestDispatcher("/WEB-INF/views/prediction.jsp").forward(request, response); + return; + } eventDAO.update(event, token); response.sendRedirect(request.getContextPath() + "/events"); } - - private String getEditTokenFromCookies(HttpServletRequest request, Long eventId) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - System.out.println("Cookie Name: " + cookie.getName() + ", Value: " + cookie.getValue()); - - if (cookie.getName().equals("event_" + eventId + "_token")) { - System.out.println("Cookie Found: " + cookie.getName() + ", Value: " + cookie.getValue()); - - return cookie.getValue(); - } - - } - - } - return null; - } } diff --git a/emmas/src/main/java/dev/brianweloba/util/CookieUtil.java b/emmas/src/main/java/dev/brianweloba/util/CookieUtil.java new file mode 100644 index 0000000..f58235f --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/util/CookieUtil.java @@ -0,0 +1,29 @@ +package dev.brianweloba.util; + +import dev.brianweloba.model.Event; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class CookieUtil { + public static void setCookie(HttpServletResponse response, Event event) { + Cookie tokenCookie = new Cookie("event_" + event.getId() + "_token", event.getEditToken()); + tokenCookie.setMaxAge(60 * 60 * 24 * 365); + tokenCookie.setPath("/"); + response.addCookie(tokenCookie); + } + + public static String getEditTokenFromCookies(HttpServletRequest request, Long eventId) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("event_" + eventId + "_token")) { + return cookie.getValue(); + } + + } + + } + return null; + } +} diff --git a/emmas/src/main/java/dev/brianweloba/util/PasswordUtil.java b/emmas/src/main/java/dev/brianweloba/util/PasswordUtil.java new file mode 100644 index 0000000..51879bc --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/util/PasswordUtil.java @@ -0,0 +1,14 @@ +package dev.brianweloba.util; + + +import org.mindrot.jbcrypt.BCrypt; + +public class PasswordUtil { + public static String hashPassword(String plainTextPassword) { + return BCrypt.hashpw(plainTextPassword, BCrypt.gensalt()); + } + + public static boolean checkPassword(String plainTextPassword, String hashedPassword) { + return BCrypt.checkpw(plainTextPassword, hashedPassword); + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/weka/ClassifierLogger.java b/emmas/src/main/java/dev/brianweloba/weka/ClassifierLogger.java new file mode 100644 index 0000000..ff9a402 --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/weka/ClassifierLogger.java @@ -0,0 +1,48 @@ +package dev.brianweloba.weka; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ClassifierLogger { + private static final String LOG_FILE = "classifier_results.log"; + private static final String METRICS_FILE = "classifier_metrics.csv"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + // Initialize metrics file with headers if it doesn't exist + static { + try (PrintWriter writer = new PrintWriter(new FileWriter(METRICS_FILE, true))) { + if (new File(METRICS_FILE).length() == 0) { + writer.println("timestamp,accuracy,precision,recall,f1,training_size,test_size"); + } + } catch (IOException e) { + System.err.println("Error initializing metrics file: " + e.getMessage()); + } + } + + public static void log(String message) { + try (PrintWriter writer = new PrintWriter(new FileWriter(LOG_FILE, true))) { + String timestamp = DATE_FORMAT.format(new Date()); + writer.printf("[%s] %s%n", timestamp, message); + System.out.printf("[%s] %s%n", timestamp, message); // Also print to console + } catch (IOException e) { + System.err.println("Error writing to log file: " + e.getMessage()); + } + } + + public static void logMetrics(double accuracy, double precision, + double recall, double f1, + int trainingSize, int testSize) { + try (PrintWriter writer = new PrintWriter(new FileWriter(METRICS_FILE, true))) { + String timestamp = DATE_FORMAT.format(new Date()); + writer.printf("%s,%.4f,%.4f,%.4f,%.4f,%d,%d%n", + timestamp, accuracy, precision, + recall, f1, trainingSize, testSize); + } catch (IOException e) { + System.err.println("Error writing metrics: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/weka/EventClassifierTester.java b/emmas/src/main/java/dev/brianweloba/weka/EventClassifierTester.java new file mode 100644 index 0000000..00162af --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/weka/EventClassifierTester.java @@ -0,0 +1,351 @@ +package dev.brianweloba.weka; + +import dev.brianweloba.model.Event; +import weka.classifiers.Evaluation; +import weka.core.Instances; +import weka.core.converters.ArffSaver; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.*; + +public class EventClassifierTester { + private final EventTypeClassifier classifier; + private final String outputDir; + private static final boolean VERBOSE_LOGGING = true; // Toggle for detailed logs + + public EventClassifierTester(String outputDir) { + this.classifier = new EventTypeClassifier(); + this.outputDir = outputDir; + new File(outputDir).mkdirs(); // Ensure directory exists + } + + public void runFullTest(List allEvents) throws Exception { + ClassifierLogger.log("Starting event classification evaluation"); + + // Ensure we have enough data + if (allEvents.size() < 50) { + ClassifierLogger.log("Warning: Small dataset size (" + allEvents.size() + " events)"); + } + + // Quick summary of class distribution + logClassDistributionSummary(allEvents); + + // Split data (70% train, 30% test) + Collections.shuffle(allEvents, new Random(42)); // Fixed seed for reproducibility + int splitPoint = (int) (allEvents.size() * 0.7); + List trainingEvents = allEvents.subList(0, splitPoint); + List testEvents = allEvents.subList(splitPoint, allEvents.size()); + + // Train model + ClassifierLogger.log("Training with " + trainingEvents.size() + " events"); + classifier.trainModel(trainingEvents); + saveModelAndData(trainingEvents, "training"); + + // Test and save results + ClassifierLogger.log("Testing with " + testEvents.size() + " events"); + TestResults results = evaluateOnTestSet(testEvents); + + // Calculate per-class metrics + Map classMetrics = calculatePerClassMetrics(testEvents); + + // Save detailed results + saveTestResults(results, testEvents, classMetrics); + + // Log summary metrics + logSummaryMetrics(results); + + // Perform cross-validation + performCrossValidation(allEvents, 5); + } + + private void logClassDistributionSummary(List events) { + Map typeCounts = new HashMap<>(); + for (Event event : events) { + if (event.getEventType() != null) { + typeCounts.merge(event.getEventType(), 1, Integer::sum); + } + } + + ClassifierLogger.log("Class distribution: " + + typeCounts.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .map(e -> String.format("%s=%d(%.1f%%)", + e.getKey(), e.getValue(), (double)e.getValue()*100/events.size())) + .reduce((a, b) -> a + ", " + b) + .orElse("")); + } + + private void logSummaryMetrics(TestResults results) { + ClassifierLogger.log(String.format( + "Results: Accuracy=%.1f%%, Precision=%.3f, Recall=%.3f, F1=%.3f", + results.accuracy * 100, results.precision, results.recall, results.f1 + )); + } + + private void performCrossValidation(List allEvents, int folds) { + try { + Random rand = new Random(42); + Collections.shuffle(allEvents, rand); + + int foldSize = allEvents.size() / folds; + double totalAccuracy = 0; + double[] foldAccuracies = new double[folds]; + + for (int i = 0; i < folds; i++) { + // Split for this fold + List cvTest = new ArrayList<>(allEvents.subList(i * foldSize, + Math.min((i + 1) * foldSize, allEvents.size()))); + List cvTrain = new ArrayList<>(allEvents); + cvTrain.removeAll(cvTest); + + // Train + EventTypeClassifier cvClassifier = new EventTypeClassifier(); + cvClassifier.trainModel(cvTrain); + + // Test + int correct = 0; + for (Event event : cvTest) { + String predicted = cvClassifier.predictEventType(event); + if (predicted.equals(event.getEventType())) { + correct++; + } + } + + foldAccuracies[i] = (double) correct / cvTest.size(); + totalAccuracy += foldAccuracies[i]; + + if (VERBOSE_LOGGING) { + ClassifierLogger.log(String.format("Fold %d accuracy: %.1f%%", + i+1, foldAccuracies[i] * 100)); + } + } + + // Report the summary with min/max/avg + double avgAccuracy = totalAccuracy / folds; + double minAccuracy = Arrays.stream(foldAccuracies).min().orElse(0); + double maxAccuracy = Arrays.stream(foldAccuracies).max().orElse(0); + + ClassifierLogger.log(String.format("CV results: Avg=%.1f%%, Min=%.1f%%, Max=%.1f%% (%d folds)", + avgAccuracy * 100, minAccuracy * 100, maxAccuracy * 100, folds)); + + } catch (Exception e) { + ClassifierLogger.log("CV error: " + e.getMessage()); + } + } + + private void saveConfusionMatrix(Evaluation eval, Instances data) { + try (PrintWriter writer = new PrintWriter(new File(outputDir + "/confusion_matrix.txt"))) { + writer.println("=== Confusion Matrix ==="); + writer.println(eval.toMatrixString()); + writer.println("\n=== Detailed Accuracy By Class ==="); + writer.println(eval.toClassDetailsString()); + } catch (Exception e) { + ClassifierLogger.log("Error saving confusion matrix: " + e.getMessage()); + } + } + + private Map calculatePerClassMetrics(List testEvents) { + Map metrics = new HashMap<>(); + Map truePositives = new HashMap<>(); + Map falsePositives = new HashMap<>(); + Map falseNegatives = new HashMap<>(); + Map totalActual = new HashMap<>(); + + // Count actual class distribution + for (Event event : testEvents) { + String actual = event.getEventType(); + totalActual.merge(actual, 1, Integer::sum); + } + + // Calculate metrics + for (Event event : testEvents) { + String actual = event.getEventType(); + String predicted = classifier.predictEventType(event); + + if (actual.equals(predicted)) { + truePositives.merge(actual, 1, Integer::sum); + } else { + falseNegatives.merge(actual, 1, Integer::sum); + falsePositives.merge(predicted, 1, Integer::sum); + } + } + + // Calculate precision, recall, and F1 for each class + for (String className : totalActual.keySet()) { + int tp = truePositives.getOrDefault(className, 0); + int fp = falsePositives.getOrDefault(className, 0); + int fn = falseNegatives.getOrDefault(className, 0); + int total = totalActual.get(className); + + double precision = (tp + fp == 0) ? 0 : (double) tp / (tp + fp); + double recall = (double) tp / total; + double f1 = (precision + recall == 0) ? 0 : 2 * precision * recall / (precision + recall); + + metrics.put(className, new ClassMetrics(precision, recall, f1, total)); + } + + return metrics; + } + + private TestResults evaluateOnTestSet(List testEvents) { + TestResults results = new TestResults(); + int correct = 0; + Map classCounts = new HashMap<>(); + Map correctCounts = new HashMap<>(); + List misclassifications = new ArrayList<>(); + + try (PrintWriter writer = new PrintWriter(new File(outputDir + "/detailed_results.csv"))) { + writer.println("actual,predicted,confidence,title,is_correct"); + + for (Event event : testEvents) { + String actual = event.getEventType(); + String predicted = classifier.predictEventType(event); + Map probs = classifier.getPredictionProbabilities(event); + double confidence = probs.getOrDefault(predicted, 0.0); + boolean isCorrect = actual.equals(predicted); + + writer.printf("%s,%s,%.4f,%s,%b%n", + actual, predicted, confidence, + event.getTitle().replace(",", ""), isCorrect); + + // Update class statistics + classCounts.merge(actual, 1, Integer::sum); + if (isCorrect) { + correct++; + correctCounts.merge(actual, 1, Integer::sum); + } else { + // Store misclassifications instead of logging each one + misclassifications.add(String.format( + "Actual=%s, Predicted=%s (%.0f%%) for '%s'", + actual, predicted, confidence*100, event.getTitle())); + } + } + + // Calculate global metrics + results.accuracy = (double) correct / testEvents.size(); + + // Calculate confusion matrix-based metrics + double totalPrecision = 0; + double totalRecall = 0; + int classCount = classCounts.size(); + + for (String className : classCounts.keySet()) { + int truePositives = correctCounts.getOrDefault(className, 0); + int total = classCounts.get(className); + + // Calculate predicted count (for precision) + long predictedCount = testEvents.stream() + .filter(e -> classifier.predictEventType(e).equals(className)) + .count(); + + double precision = predictedCount == 0 ? 0 : (double) truePositives / predictedCount; + double recall = (double) truePositives / total; + + totalPrecision += precision; + totalRecall += recall; + } + + // Calculate macro-averaged metrics + results.precision = totalPrecision / classCount; + results.recall = totalRecall / classCount; + results.f1 = (results.precision + results.recall == 0) ? 0 : + 2 * results.precision * results.recall / (results.precision + results.recall); + + // Log misclassification summary + int misclassificationCount = misclassifications.size(); + if (misclassificationCount > 0) { + ClassifierLogger.log(String.format("Misclassifications: %d/%d (%.1f%%)", + misclassificationCount, testEvents.size(), + (double)misclassificationCount*100/testEvents.size())); + + // Only log the first few misclassifications if verbose + if (VERBOSE_LOGGING) { + int limit = Math.min(5, misclassificationCount); + for (int i = 0; i < limit; i++) { + ClassifierLogger.log("MISCLASSIFIED: " + misclassifications.get(i)); + } + if (misclassificationCount > limit) { + ClassifierLogger.log("... and " + (misclassificationCount - limit) + " more"); + } + } + } + + } catch (Exception e) { + ClassifierLogger.log("Error during testing: " + e.getMessage()); + } + + return results; + } + + private void saveModelAndData(List events, String prefix) throws Exception { + // Save ARFF file for inspection + Instances data = classifier.getTrainingData(); + ArffSaver saver = new ArffSaver(); + saver.setInstances(data); + saver.setFile(new File(outputDir + "/" + prefix + "_data.arff")); + saver.writeBatch(); + + // Save model + classifier.saveModel(outputDir + "/" + prefix + "_model.model"); + } + + private void saveTestResults(TestResults results, List testEvents, + Map classMetrics) { + try (PrintWriter writer = new PrintWriter(new File(outputDir + "/summary_results.txt"))) { + writer.println("=== Classifier Test Results ==="); + writer.printf("Accuracy: %.2f%%%n", results.accuracy * 100); + writer.printf("Macro-Averaged Precision: %.4f%n", results.precision); + writer.printf("Macro-Averaged Recall: %.4f%n", results.recall); + writer.printf("Macro-Averaged F1 Score: %.4f%n", results.f1); + + writer.println("\n=== Per-Class Performance ==="); + for (Map.Entry entry : classMetrics.entrySet()) { + ClassMetrics metrics = entry.getValue(); + writer.printf("%s (n=%d):%n", entry.getKey(), metrics.count); + writer.printf(" Precision: %.4f%n", metrics.precision); + writer.printf(" Recall: %.4f%n", metrics.recall); + writer.printf(" F1 Score: %.4f%n", metrics.f1); + } + + writer.println("\n=== Test Set Distribution ==="); + + // Count actual class distribution + Map actualCounts = new HashMap<>(); + for (Event e : testEvents) { + actualCounts.merge(e.getEventType(), 1, Integer::sum); + } + + for (Map.Entry entry : actualCounts.entrySet()) { + writer.printf("%s: %d (%.1f%%)%n", + entry.getKey(), entry.getValue(), + (double) entry.getValue() / testEvents.size() * 100); + } + + } catch (IOException e) { + ClassifierLogger.log("Error saving test results: " + e.getMessage()); + } + } + + private static class TestResults { + double accuracy; + double precision; + double recall; + double f1; + } + + private static class ClassMetrics { + double precision; + double recall; + double f1; + int count; + + public ClassMetrics(double precision, double recall, double f1, int count) { + this.precision = precision; + this.recall = recall; + this.f1 = f1; + this.count = count; + } + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/weka/EventTypeClassifier.java b/emmas/src/main/java/dev/brianweloba/weka/EventTypeClassifier.java index 4640ef0..26435cd 100644 --- a/emmas/src/main/java/dev/brianweloba/weka/EventTypeClassifier.java +++ b/emmas/src/main/java/dev/brianweloba/weka/EventTypeClassifier.java @@ -1,136 +1,442 @@ package dev.brianweloba.weka; import dev.brianweloba.model.Event; -import weka.classifiers.trees.RandomForest; -import weka.core.Attribute; -import weka.core.DenseInstance; -import weka.core.Instance; -import weka.core.Instances; +import weka.classifiers.bayes.NaiveBayes; +import weka.classifiers.evaluation.Evaluation; +import weka.classifiers.meta.AdaBoostM1; +import weka.core.*; +import weka.core.stopwords.Rainbow; +import weka.core.tokenizers.NGramTokenizer; import weka.filters.Filter; +import weka.filters.supervised.instance.SMOTE; import weka.filters.unsupervised.attribute.StringToWordVector; +import weka.classifiers.meta.CVParameterSelection; import java.util.*; +import java.util.stream.Collectors; public class EventTypeClassifier { - private RandomForest classifier; + private static final int MIN_CLASSES_FOR_CLASSIFICATION = 2; + private static final int MIN_INSTANCES_FOR_BALANCING = 6; + private static final int SMOTE_NEIGHBORS = 5; + private static final int SMOTE_PERCENTAGE = 100; + private static final int NGRAM_MIN_SIZE = 1; + private static final int NGRAM_MAX_SIZE = 2; + private static final int WORDS_TO_KEEP = 500; + private static final int RANDOM_SEED = 42; + + // private NaiveBayes classifier; + private AdaBoostM1 classifier; private Instances trainingHeader; public void trainModel(List events) { - try{ - ArrayList attributes = createAttributes(); - - Set uniqueEventTypes = new HashSet<>(); - for(Event event: events){ - if(event.getEventType() != null){ - uniqueEventTypes.add(event.getEventType()); - } - } + try { + List validEvents = validateAndFilterEvents(events); + logClassDistribution(validEvents); + + Instances trainingData = createTrainingDataset(validEvents); + Instances filteredData = applyTextProcessingFilters(trainingData); + Instances balancedData = balanceDataset(filteredData); + + trainClassifier(balancedData); + evaluateModel(balancedData); + + // Add this after model training: + ClassifierLogger.log("\n=== AdaBoost Configuration ==="); + ClassifierLogger.log("Iterations: " + classifier.getNumIterations()); + ClassifierLogger.log("Using classifier: " + classifier.getClassifier().getClass().getSimpleName()); + } catch (Exception e) { + throw new RuntimeException("Failed to train model: " + e.getMessage(), e); + } + } + + private List validateAndFilterEvents(List events) { + List validEvents = events.stream() + .filter(e -> e.getEventType() != null && !e.getEventType().isEmpty()) + .toList(); + + if (validEvents.isEmpty()) { + throw new IllegalArgumentException("No valid events with eventType available for training"); + } + + ClassifierLogger.log("Starting training with " + validEvents.size() + " events"); + + Set uniqueEventTypes = validEvents.stream() + .map(Event::getEventType) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (uniqueEventTypes.size() < MIN_CLASSES_FOR_CLASSIFICATION) { + throw new IllegalArgumentException("Need at least " + MIN_CLASSES_FOR_CLASSIFICATION + + " different event types for classification"); + } + + return validEvents; + } + + private void logClassDistribution(List events) { + Map classCounts = events.stream() + .collect(Collectors.groupingBy(Event::getEventType, Collectors.counting())); + + ClassifierLogger.log("Class distribution in training data:"); + classCounts.forEach((type, count) -> + ClassifierLogger.log(String.format(" %s: %d (%.1f%%)", + type, count, (double) count * 100 / events.size()))); + } + + private Instances createTrainingDataset(List events) { + Set uniqueEventTypes = events.stream() + .map(Event::getEventType) + .collect(Collectors.toSet()); + + ArrayList attributes = createAttributes(); + Attribute classAttribute = new Attribute("class_event_type", new ArrayList<>(uniqueEventTypes)); + attributes.add(classAttribute); + + Instances trainingData = new Instances("EventData", attributes, 0); + trainingData.setClassIndex(trainingData.numAttributes() - 1); + + for (Event event : events) { + addInstance(trainingData, event); + } + + return trainingData; + } + + private Instances applyTextProcessingFilters(Instances data) throws Exception { + int classIndex = data.classIndex(); + Attribute classAttr = data.attribute(classIndex); + + Instances dataForFiltering = new Instances(data); + dataForFiltering.setClassIndex(-1); + dataForFiltering.deleteAttributeAt(classIndex); + + StringToWordVector filter = createTextFilter(); + filter.setInputFormat(dataForFiltering); + Instances filteredData = Filter.useFilter(dataForFiltering, filter); + + filteredData.insertAttributeAt(classAttr, filteredData.numAttributes()); + filteredData.setClassIndex(filteredData.numAttributes() - 1); + + for (int i = 0; i < data.numInstances(); i++) { + filteredData.instance(i).setValue(filteredData.classIndex(), + data.instance(i).value(classIndex)); + } + + return filteredData; + } + + private StringToWordVector createTextFilter() { + StringToWordVector filter = new StringToWordVector(); + filter.setAttributeIndices("1,2,3,4"); // Text attributes + filter.setWordsToKeep(WORDS_TO_KEEP); + filter.setTFTransform(true); + filter.setIDFTransform(true); + + NGramTokenizer tokenizer = new NGramTokenizer(); + tokenizer.setNGramMinSize(NGRAM_MIN_SIZE); + tokenizer.setNGramMaxSize(NGRAM_MAX_SIZE); + filter.setTokenizer(tokenizer); + + filter.setStopwordsHandler(new Rainbow()); + filter.setStemmer(new weka.core.stemmers.LovinsStemmer()); + + return filter; + } + + private Instances balanceDataset(Instances data) throws Exception { + Set uniqueClasses = new HashSet<>(); + for (int i = 0; i < data.numInstances(); i++) { + uniqueClasses.add(data.instance(i).classValue()); + } + + if (data.numInstances() < uniqueClasses.size() * MIN_INSTANCES_FOR_BALANCING) { + ClassifierLogger.log("Not enough instances for balancing, using original data"); + return data; + } + + try { + return applyAdvancedBalancing(data); + } catch (Exception e) { + ClassifierLogger.log("Advanced balancing failed: " + e.getMessage()); + ClassifierLogger.log("Falling back to basic SMOTE"); + return applyBasicSMOTE(data); + } + } + + private Instances applyAdvancedBalancing(Instances data) throws Exception { + int maxClassSize = 0; + Map classInstanceCounts = new HashMap<>(); + + for (int i = 0; i < data.numInstances(); i++) { + double classValue = data.instance(i).classValue(); + classInstanceCounts.merge(classValue, 1, Integer::sum); + maxClassSize = Math.max(maxClassSize, classInstanceCounts.get(classValue)); + } - ArrayList eventTypes = new ArrayList<>(uniqueEventTypes); + Instances balancedData = new Instances(data, 0); - attributes.add(new Attribute("eventType", eventTypes)); - Instances trainingData = new Instances("EventData", attributes, 0); - trainingData.setClassIndex(trainingData.numAttributes() - 1); + for (double classValue : classInstanceCounts.keySet()) { + Instances classData = extractClassInstances(data, classValue); + int targetSize = maxClassSize / 2; - for(Event event: events){ - if(event.getEventType() != null){ - addInstance(trainingData, event); - } + if (classInstanceCounts.get(classValue) > targetSize) { + balancedData.addAll(downsampleClass(classData, targetSize)); + } else { + balancedData.addAll(oversampleClassWithSMOTE(classData, targetSize)); } + } + + logBalancedDistribution(balancedData); + return balancedData; + } + + private Instances extractClassInstances(Instances data, double classValue) { + Instances classData = new Instances(data, 0); + for (int i = 0; i < data.numInstances(); i++) { + if (data.instance(i).classValue() == classValue) { + classData.add(data.instance(i)); + } + } + return classData; + } + + private Instances downsampleClass(Instances classData, int targetSize) { + classData.randomize(new Random(RANDOM_SEED)); + return new Instances(classData, 0, Math.min(targetSize, classData.numInstances())); + } + + private Instances oversampleClassWithSMOTE(Instances classData, int targetSize) throws Exception { + int currentSize = classData.numInstances(); + int percentage = (targetSize * 100) / currentSize - 100; - Instances filteredData = applyStringToWordVectorFilter(trainingData); + SMOTE smote = new SMOTE(); + smote.setInputFormat(classData); + smote.setNearestNeighbors(Math.min(SMOTE_NEIGHBORS, currentSize - 1)); + smote.setPercentage(Math.max(100, percentage)); - classifier = new RandomForest(); - classifier.setMaxDepth(0); - classifier.buildClassifier(filteredData); + return Filter.useFilter(classData, smote); + } - trainingHeader = new Instances(filteredData, 0); + private Instances applyBasicSMOTE(Instances data) { + try { + SMOTE smote = new SMOTE(); + smote.setInputFormat(data); + smote.setNearestNeighbors(SMOTE_NEIGHBORS); + smote.setPercentage(SMOTE_PERCENTAGE); + + Instances balancedData = Filter.useFilter(data, smote); + logBalancedDistribution(balancedData); + return balancedData; } catch (Exception e) { - throw new RuntimeException("Failed to train model: " + e.getMessage(),e); + ClassifierLogger.log("SMOTE failed, using original data: " + e.getMessage()); + return data; } } - private Instances applyStringToWordVectorFilter(Instances data) throws Exception { - StringToWordVector filter = new StringToWordVector(); - filter.setAttributeIndices("1,2,3,4,5,6"); - filter.setWordsToKeep(1000); - filter.setLowerCaseTokens(true); - filter.setInputFormat(data); - return Filter.useFilter(data, filter); + private void logBalancedDistribution(Instances data) { + Map balancedCounts = new HashMap<>(); + for (int i = 0; i < data.numInstances(); i++) { + double classValue = data.instance(i).classValue(); + balancedCounts.merge(classValue, 1, Integer::sum); + } + + ClassifierLogger.log("Class distribution after balancing:"); + balancedCounts.forEach((classVal, count) -> { + String className = data.classAttribute().value(classVal.intValue()); + ClassifierLogger.log(String.format(" %s: %d (%.1f%%)", + className, count, (double) count * 100 / data.numInstances())); + }); + } + + private void configureAdaBoost(AdaBoostM1 adaBoost) { + adaBoost.setDebug(false); + adaBoost.setNumIterations(15); // Can experiment with 10-20 + adaBoost.setWeightThreshold(50); + adaBoost.setUseResampling(false); + adaBoost.setResume(false); + } + +// private void trainClassifier(Instances data) throws Exception { +// // 1. Create base classifier +// NaiveBayes nb = new NaiveBayes(); +// nb.setUseKernelEstimator(false); +// nb.setUseSupervisedDiscretization(true); +// +// // 2. Create AdaBoost with default parameters +// AdaBoostM1 baseAdaBoost = new AdaBoostM1(); +// baseAdaBoost.setClassifier(nb); +// baseAdaBoost.setSeed(RANDOM_SEED); +// +// // 3. Setup parameter search +// CVParameterSelection ps = new CVParameterSelection(); +// ps.setClassifier(baseAdaBoost); +// ps.setNumFolds(5); // 5-fold cross-validation +// +// // Fix: Correct parameter flag for iterations (should be "I" not "P") +// ps.addCVParameter("I 10 20 10"); // Tests 10, 15, 20 iterations +// +// // Optional: Search weightThreshold too +// // ps.addCVParameter("W 50 150 50"); // Tests 50, 100, 150 +// +// // 4. Run optimization +// ps.buildClassifier(data); +// +// // 5. Apply best parameters manually +// classifier = new AdaBoostM1(); +// classifier.setClassifier(nb); +// +// // Get best options and apply them +// String[] bestOptions = ps.getBestClassifierOptions(); +// classifier.setOptions(bestOptions); +// +// // Fix: Train the classifier with the optimized parameters +// classifier.buildClassifier(data); +// +// // 6. Log the selected parameters +// ClassifierLogger.log("\n=== Optimized AdaBoost Parameters ==="); +// ClassifierLogger.log("Selected iterations: " + classifier.getNumIterations()); +// ClassifierLogger.log("Best configuration: " + Arrays.toString(bestOptions)); +// +// trainingHeader = new Instances(data, 0); +// } + + private void trainClassifier(Instances data) throws Exception { + // 1. Create base classifier + NaiveBayes nb = new NaiveBayes(); + nb.setUseKernelEstimator(false); + nb.setUseSupervisedDiscretization(true); + + // 2. Perform manual parameter search for AdaBoost + int[] iterationsToTest = {5, 10, 15, 20, 25}; // The iterations we want to test + double bestAccuracy = 0.0; + int bestIterations = 10; // Default value + + ClassifierLogger.log("\n=== Manual Parameter Search ==="); + + for (int iterations : iterationsToTest) { + // Create and configure a fresh AdaBoost instance for each test + AdaBoostM1 testBoost = new AdaBoostM1(); + testBoost.setClassifier(nb); + testBoost.setSeed(RANDOM_SEED); + testBoost.setNumIterations(iterations); + + // Evaluate using cross-validation + Evaluation eval = new Evaluation(data); + eval.crossValidateModel(testBoost, data, 5, new Random(RANDOM_SEED)); + + double accuracy = eval.pctCorrect(); + ClassifierLogger.log(String.format(" Iterations=%d, Accuracy=%.2f%%", + iterations, accuracy)); + + // Keep track of the best configuration + if (accuracy > bestAccuracy) { + bestAccuracy = accuracy; + bestIterations = iterations; + } + } + + // 3. Create the final classifier with the best parameters + classifier = new AdaBoostM1(); + classifier.setClassifier(nb); + classifier.setSeed(RANDOM_SEED); + classifier.setNumIterations(bestIterations); + + // Train the final model + classifier.buildClassifier(data); + + // 4. Log the selected parameters + ClassifierLogger.log("\n=== Optimized AdaBoost Parameters ==="); + ClassifierLogger.log("Selected iterations: " + bestIterations); + ClassifierLogger.log("Best accuracy: " + String.format("%.2f%%", bestAccuracy)); + ClassifierLogger.log("Using classifier: " + classifier.getClassifier().getClass().getSimpleName()); + + trainingHeader = new Instances(data, 0); + } + + private void evaluateModel(Instances data) throws Exception { + Evaluation eval = new Evaluation(data); + eval.crossValidateModel(classifier, data, 10, new Random(RANDOM_SEED)); + + ClassifierLogger.log("=== Classification Results ==="); + ClassifierLogger.log(eval.toSummaryString()); + + ClassifierLogger.log("\n=== Detailed Accuracy By Class ==="); + ClassifierLogger.log(eval.toClassDetailsString()); + + ClassifierLogger.log("\n=== Confusion Matrix ==="); + ClassifierLogger.log(eval.toMatrixString()); } private ArrayList createAttributes() { ArrayList attributes = new ArrayList<>(); + + // Text attributes attributes.add(new Attribute("title", (List) null)); attributes.add(new Attribute("description", (List) null)); attributes.add(new Attribute("hostName", (List) null)); attributes.add(new Attribute("location", (List) null)); - attributes.add(new Attribute("eventCapacity")); // Numeric - attributes.add(new Attribute("eventMonth")); - return attributes; - } - - private Instances createFilteredInstance(Event event) throws Exception { - ArrayList originalAttributes = createAttributes(); - originalAttributes.add(trainingHeader.classAttribute()); - - Calendar cal = Calendar.getInstance(); - cal.setTime(event.getEventDate()); - int month = cal.get(Calendar.MONTH) + 1; - Instances tempData = new Instances("TempData", originalAttributes, 0); - tempData.setClassIndex(tempData.numAttributes() - 1); - - double[] values = addValuesToDataset(event, month, tempData); - values[6] = 0; + // Numeric attributes + attributes.add(new Attribute("eventCapacity")); + attributes.add(new Attribute("eventMonth")); + attributes.add(new Attribute("eventDayOfWeek")); + attributes.add(new Attribute("isWeekend")); - tempData.add(new DenseInstance(1.0, values)); - return applyStringToWordVectorFilter(tempData); - } + // Enhanced features + attributes.add(new Attribute("titleLength")); + attributes.add(new Attribute("descriptionWordCount")); + attributes.add(new Attribute("hasWorkshopInTitle")); // Binary + attributes.add(new Attribute("hasWorkshopInDesc")); // Binary + attributes.add(new Attribute("hasHandsOnInDesc")); // Binary - private double[] addValuesToDataset(Event event, int month, Instances tempData) { - double[] values = new double[tempData.numAttributes()]; - values[0] = tempData.attribute(0).addStringValue(Objects.toString(event.getTitle(), "")); - values[1] = tempData.attribute(1).addStringValue(Objects.toString(event.getDescription(), "")); - values[2] = tempData.attribute(2).addStringValue(Objects.toString(event.getEventHost(), "")); - values[3] = tempData.attribute(3).addStringValue(Objects.toString(event.getEventLocation(), "")); - values[4] = event.getEventCapacity(); - values[5] = month; - return values; + return attributes; } private void addInstance(Instances data, Event event) { - if (data.numAttributes() < 7) { - throw new IllegalStateException("Dataset does not have enough attributes! Found: " + data.numAttributes()); - } + double[] values = new double[data.numAttributes()]; + // Basic features + values[0] = data.attribute(0).addStringValue(preprocessText(event.getTitle())); + values[1] = data.attribute(1).addStringValue(preprocessText(event.getDescription())); + values[2] = data.attribute(2).addStringValue(preprocessText(event.getEventHost())); + values[3] = data.attribute(3).addStringValue(preprocessText(event.getEventLocation())); + values[4] = event.getEventCapacity(); + + // Temporal features Calendar cal = Calendar.getInstance(); cal.setTime(event.getEventDate()); - int month = cal.get(Calendar.MONTH) + 1; + values[5] = cal.get(Calendar.MONTH) + 1; + values[6] = cal.get(Calendar.DAY_OF_WEEK); + values[7] = (values[6] == Calendar.SUNDAY || values[6] == Calendar.SATURDAY) ? 1 : 0; - int eventTypeIndex = data.attribute(6).indexOfValue(event.getEventType()); - if (eventTypeIndex == -1) { - System.out.println("Warning: Event type not found - " + event.getEventType()); - eventTypeIndex = 0; - } + // Text features + String title = event.getTitle() != null ? event.getTitle().toLowerCase() : ""; + String desc = event.getDescription() != null ? event.getDescription().toLowerCase() : ""; + values[8] = title.length(); + values[9] = desc.split("\\s+").length; + + // Workshop-specific features + values[10] = title.contains("workshop") ? 1 : 0; + values[11] = desc.contains("workshop") ? 1 : 0; + values[12] = desc.contains("hands-on") || desc.contains("practical") ? 1 : 0; + + // Class value + values[data.classIndex()] = data.classAttribute().indexOfValue(event.getEventType()); - double[] values = addValuesToDataset(event, month, data); - values[6] = eventTypeIndex; data.add(new DenseInstance(1.0, values)); } + private String preprocessText(String text) { + if (text == null) return ""; + return text.toLowerCase().replaceAll("\\s+", " ").trim(); + } public String predictEventType(Event event) { try { - - Instance instance = new DenseInstance(trainingHeader.numAttributes()); - instance.setDataset(trainingHeader); - Instances filteredData = createFilteredInstance(event); - double prediction = classifier.classifyInstance(filteredData.firstInstance()); return trainingHeader.classAttribute().value((int) prediction); - } catch (Exception e) { throw new RuntimeException("Failed to make prediction: " + e.getMessage()); } @@ -138,24 +444,69 @@ public String predictEventType(Event event) { public Map getPredictionProbabilities(Event event) { try { - - ArrayList originalAttributes = createAttributes(); - originalAttributes.add(trainingHeader.classAttribute()); - Instances filteredData = createFilteredInstance(event); - double[] distributions = classifier.distributionForInstance(filteredData.firstInstance()); - Map probabilities = new HashMap<>(); + Map probabilities = new HashMap<>(); for (int i = 0; i < distributions.length; i++) { String type = trainingHeader.classAttribute().value(i); probabilities.put(type, distributions[i]); } - return probabilities; - } catch (Exception e) { throw new RuntimeException("Failed to get prediction probabilities: " + e.getMessage()); } } -} + + private Instances createFilteredInstance(Event event) throws Exception { + ArrayList originalAttributes = createAttributes(); + originalAttributes.add(trainingHeader.classAttribute()); + + Calendar cal = Calendar.getInstance(); + cal.setTime(event.getEventDate()); + int month = cal.get(Calendar.MONTH) + 1; + + Instances tempData = new Instances("TempData", originalAttributes, 0); + tempData.setClassIndex(tempData.numAttributes() - 1); + + double[] values = addValuesToDataset(event, month, tempData); + values[6] = 0; // Reset day of week for prediction + + tempData.add(new DenseInstance(1.0, values)); + return applyTextProcessingFilters(tempData); + } + + private double[] addValuesToDataset(Event event, int month, Instances tempData) { + double[] values = new double[tempData.numAttributes()]; + + // Basic features + values[0] = tempData.attribute(0).addStringValue(Objects.toString(event.getTitle(), "")); + values[1] = tempData.attribute(1).addStringValue(Objects.toString(event.getDescription(), "")); + values[2] = tempData.attribute(2).addStringValue(Objects.toString(event.getEventHost(), "")); + values[3] = tempData.attribute(3).addStringValue(Objects.toString(event.getEventLocation(), "")); + values[4] = event.getEventCapacity(); + + // Temporal features + Calendar cal = Calendar.getInstance(); + cal.setTime(event.getEventDate()); + values[5] = month; + values[6] = cal.get(Calendar.DAY_OF_WEEK); + values[7] = (values[6] == 1 || values[6] == 7) ? 1 : 0; // isWeekend + + // Textual features + String title = Objects.toString(event.getTitle(), ""); + String description = Objects.toString(event.getDescription(), ""); + values[8] = title.length(); + values[9] = description.split("\\s+").length; + + return values; + } + + public void saveModel(String filePath) throws Exception { + SerializationHelper.write(filePath, classifier); + } + + public Instances getTrainingData() { + return new Instances(trainingHeader); + } +} \ No newline at end of file diff --git a/emmas/src/main/java/dev/brianweloba/weka/RunTest.java b/emmas/src/main/java/dev/brianweloba/weka/RunTest.java new file mode 100644 index 0000000..e1ff1e3 --- /dev/null +++ b/emmas/src/main/java/dev/brianweloba/weka/RunTest.java @@ -0,0 +1,26 @@ +package dev.brianweloba.weka; + +import dev.brianweloba.dao.EventDAO; +import dev.brianweloba.model.Event; + +import java.util.List; + +public class RunTest { + public static void main(String[] args) { + try { + List allEvents = loadAllEvents(); + EventClassifierTester tester = new EventClassifierTester("improved_classifier_results"); + + tester.runFullTest(allEvents); + ClassifierLogger.log("Testing completed successfully"); + } catch (Exception e) { + ClassifierLogger.log("Fatal error in testing: " + e.getMessage()); + e.printStackTrace(); + } + } + + private static List loadAllEvents() { + EventDAO eventDAO = new EventDAO(); + return eventDAO.findAll(); + } +} diff --git a/emmas/src/main/resources/META-INF/persistence.xml b/emmas/src/main/resources/META-INF/persistence.xml index 1de4697..fa78cff 100644 --- a/emmas/src/main/resources/META-INF/persistence.xml +++ b/emmas/src/main/resources/META-INF/persistence.xml @@ -8,6 +8,7 @@ dev.brianweloba.model.Event dev.brianweloba.model.RSVP + dev.brianweloba.model.User false diff --git a/emmas/src/main/webapp/WEB-INF/components/footer.jsp b/emmas/src/main/webapp/WEB-INF/components/footer.jsp index 376a0b6..a3583d0 100644 --- a/emmas/src/main/webapp/WEB-INF/components/footer.jsp +++ b/emmas/src/main/webapp/WEB-INF/components/footer.jsp @@ -79,7 +79,7 @@ Designed by Brian Weloba

- © <%= new java.util.Date().getYear() + 1900 %> Emma's. All rights reserved. + © <%= java.time.LocalDate.now().getYear() %> Emma's. All rights reserved.

diff --git a/emmas/src/main/webapp/WEB-INF/components/navbar.jsp b/emmas/src/main/webapp/WEB-INF/components/navbar.jsp index c749545..b3c5ef6 100644 --- a/emmas/src/main/webapp/WEB-INF/components/navbar.jsp +++ b/emmas/src/main/webapp/WEB-INF/components/navbar.jsp @@ -2,40 +2,48 @@ \ No newline at end of file + diff --git a/emmas/src/main/webapp/WEB-INF/views/attendees.jsp b/emmas/src/main/webapp/WEB-INF/views/attendees.jsp new file mode 100644 index 0000000..8d0d64d --- /dev/null +++ b/emmas/src/main/webapp/WEB-INF/views/attendees.jsp @@ -0,0 +1,59 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + +
+
+ + + Showing ${attendees.size()} RSVPs with a total of ${totalGuests} attendees + + + No RSVPs yet + + +
+ +
+ + +
    + +
  • +
    +
    + + + +
    +
    +

    + +

    +

    + +

    +
    +
    + + + +${attendee.guests - 1} guests + + + 1 person + + +
    +
    +
  • +
    +
+
+ +

No one has RSVP'd to this event yet.

+
+
+
+
\ No newline at end of file diff --git a/emmas/src/main/webapp/WEB-INF/views/create.jsp b/emmas/src/main/webapp/WEB-INF/views/create.jsp index 6253659..b3cc864 100644 --- a/emmas/src/main/webapp/WEB-INF/views/create.jsp +++ b/emmas/src/main/webapp/WEB-INF/views/create.jsp @@ -9,7 +9,7 @@ let isValid = true; const title = document.getElementById('title').value; const eventHost = document.getElementById('eventHost').value; - const eventLocation = document.getElementById('location').value; + const eventLocation = document.getElementById('eventLocation').value; const eventCapacity = document.getElementById('eventCapacity').value; const eventDate = document.getElementById('eventDate').value; @@ -134,8 +134,8 @@
- - Location + ${requestScope.locationError} diff --git a/emmas/src/main/webapp/WEB-INF/views/error.jsp b/emmas/src/main/webapp/WEB-INF/views/error.jsp index 7f911bd..6c6d817 100644 --- a/emmas/src/main/webapp/WEB-INF/views/error.jsp +++ b/emmas/src/main/webapp/WEB-INF/views/error.jsp @@ -11,13 +11,23 @@ <%@ include file="../components/navbar.jsp" %> -
-

Oops, Something went wrong

-
-

- Go Home +
+

Oops, something went wrong.

+ +
+

+ +

+ + + Go Home +
+ <%@ include file="../components/footer.jsp" %> diff --git a/emmas/src/main/webapp/WEB-INF/views/events.jsp b/emmas/src/main/webapp/WEB-INF/views/events.jsp index cf02c98..32a501e 100644 --- a/emmas/src/main/webapp/WEB-INF/views/events.jsp +++ b/emmas/src/main/webapp/WEB-INF/views/events.jsp @@ -25,7 +25,7 @@ function changePageSize(size) { const url = new URL(window.location.href); - url.searchParams.set('page', 1); + url.searchParams.set('page', '1'); url.searchParams.set('size', size); window.location.href = url.toString(); } @@ -102,6 +102,51 @@ } }); } + + // For delete confirmation modal + function showDeleteConfirmation(eventId) { + const modal = document.getElementById('deleteModal'); + const confirmButton = document.getElementById('confirmDelete'); + const basePath = "${pageContext.request.contextPath}"; + // Set the correct delete URL + confirmButton.href = basePath + "/events/delete?id=" + eventId; + + // Show the modal + modal.classList.remove('hidden'); + } + + function closeDeleteModal() { + document.getElementById('deleteModal').classList.add('hidden'); + } + + // Toast notification function + function showToast(message) { + const toast = document.getElementById('toast'); + const toastMessage = document.getElementById('toastMessage'); + + toastMessage.textContent = message; + + // Show the toast (both transform and opacity) + toast.classList.remove('translate-y-full'); + toast.classList.remove('opacity-0'); + toast.classList.remove('pointer-events-none'); + + // Hide the toast after 3 seconds + setTimeout(function () { + toast.classList.add('translate-y-full'); + toast.classList.add('opacity-0'); + toast.classList.add('pointer-events-none'); + }, 3000); + } + + document.addEventListener('DOMContentLoaded', function () { + const statusMessage = "${sessionScope.statusMessage}"; + + if (statusMessage && statusMessage.trim() !== "") { + showToast(statusMessage); + <% session.removeAttribute("statusMessage"); %> + } + }); @@ -116,20 +161,6 @@
Showing ${requestScope.startIndex +1} to ${requestScope.endIndex} of ${requestScope.totalEvents} entries
- -
-
- -
- -