Komunikator grupowy to aplikacja webowa, w której możemy stworzyć profil użytkownika, dodawać nowe grupy, przypisywać do nich członków i tworzyć konwersacje grupowe za pomocą przesyłanych maili.
Projekt realizowany jest w języku Java z wykorzystaniem frameworka Hibernate do realizacji warstwy dostępu do danych oraz relacyjnej bazy danych Microsoft SQL Server w celu ich przechowywania.
Interfejs tworzą strony JSP napisane w formacie HTML z wykorzystaniem servletów przetwarzających żądania HTTP.
-
Gabriel Cyganek
-
Krzysztof Siekierzyński
-
Joanna Klimek
Aplikacja pozwala na tworzenie grup, w których każdy członek może wysłać wiadomość do całej grupy. Jeśli użytkownik chce dołączyć do grupy, musi ją wyszukać w aplikacji i złożyć wniosek o dołączenie. Administrator oraz moderator może zaakceptować lub odrzucić taki wniosek. Każdy użytkownik może przeglądać wiadomości danej grupy oraz wszystkie wiadomości z grup, do których należy.
Każdy użytkownik ma jedną z trzech ról w grupie, zwykły członek może tylko wysyłać i przeglądać wiadomości, moderator może akceptować wnioski o dołączenie do grupy oraz usuwać zwykłych członków z niej, a administrator może to co moderator, oraz usuwać moderatorów i nadawać rangi.
Możliwa jest również edycja danych użytkownika i grupy, a także usunięcie profilu danej osoby.
W folderze src/main/webapp znajdują się pliki .jsp, które tworzą interfejs. Za komunikację z bazą danych odpowiedzialne są servlety znajdujące się w src/main/java/agh/edu/pl/servlets. Każdy servlet ma w komentarzu nad kodem napisane, za co jest odpowiedzialny. Tabele generujące schemat bazy danych są w src/main/java/agh/edu/pl/tables.
| Tabela | Znaczenie |
|---|---|
| User | Przechowuje dane użytkowników, takie jak imie, nazwisko, adres, e-mail, data urodzenia |
| Group | Przechowuje podstawowe dane na temat grup, czyli nazwę i opis |
| GroupRequest | Realizując relację wiele-do-wielu, informuje którzy użytkownicy wysłali prośbę o dołączenie do poszczególnych grup |
| GroupMember | Realizując relację wiele-do-wielu, informuje jacy użytkownicy aktualnie przynależą do poszczególnych grup oraz jaką posiadają w rangę w danej grupie (admin/moderator/member) |
| Przechowuje podstawowe informacje dotyczące wysłanych maili | |
| Inbox | Realizując relację wiele-do-wielu, przypisuje mailom ich odbiorców z tabeli User oraz przechowuje informację, czy mail został przeczytany przez danego użytkownika |
| Outbox | Realizując relację wiele-do-wielu, przypisuje mailom ich nadawców z tabeli User oraz przechowuje informację, czy mail został usunięty ze skrzynki nadawczej przez nadawcę |
Schemat bazy wynika ze sposobu działania aplikacji. Stawiając na komunikację grupową postanowiliśmy przypisać maile do grup zamiast użytkowników aplikacji. Jako że wiadomości przesyłane są do wielu użytkowników jednocześnie, zdecydowaliśmy się przechowywać jedną kopię maila w tabeli Mail oraz wiele wpisów w tabelach Inbox i Outbox, które informują o tym do kogo i od kogo maile wiadomości zostały wysłane. Natomiast podział na skrzynkę nadawcza i odbiorczą umożliwia nam śledzenie takich informacji jak fakt czy mail został przeczytany przez konkretnego odbiorcę, bądź usunięty z prywatnej skrzynki nadawczej przez nadawcę.
git clone https://github.com/ksiek127/komunikator GroupCommunicator
cd GroupCommunicator
gradle build
Uruchamianie aplikacji z użyciem serwera Apache Tomcat 10.0.6 przy pomocy IDE IntelliJ IDEA 2021.1.1 (Ultimate Edition)
Po otwarciu folderu z zaimportowanym projektem w IntelliJ należy zainstalować serwer Apache Tomcat 10.0.6 stosując następujące kroki:
-
Dodać nową konfigurację aplikacji (
Run/Edit Configurationsa następnieAdd New Configuration) i wybraćTomcat Server Local
-
Wskazać katalog, gdzie rozpakowano pobrany serwer Apache Tomcat 10.0.6 po naciśnięciu na
Configure...przy opcjiApplication Serveroraz nacisnąć na przyciskFixw prawym dolnym rogu przy ostrzeżeniuWarning: No artifacts marked for deploymenti wybrać domyślną wersję war
-
Po zaakceptowaniu wprowadzonej konfiguracji można uruchomić aplikację

Aby zmienić serwer bazodanowy do którego będzie podłączona aplikacja należy odpowiednio zmodyfikować plik hibernate.cfg.xml
Aby aplikacja działała poprawnie, należy upewnić się, że w bazie danych w tabeli User występuje użytkownik o adresie email: deleted@account. Polecenie dodające wspomnianego użytkownika do bazy danych:
INSERT INTO [User] (city, country, street, zipCode, birthDate, email, firstname, lastname) VALUES ('City', 'Country', 'Street', '11-111', '2000-01-01 12:00:00.000', 'deleted@account', 'Account', 'Deleted')
Opis oparty na Servletcie GroupsSearchServlet przetwarzającym żądania Get oraz Post.
Na początku każdego Servletu opisane jest w komentarzu jego działanie
/*
On Get: Redirects to searchGroups.jsp which shows a groups search engine
On Post: Uses group_name parameter provided by the user using the groups search engine. Creates a groups map and loads
all groups with names matching the group_name parameter. Checks if user is already a member, requested joining or
can request to join the group. Redirects to searchGroups.jsp with a search result.
*/
Następnie wykorzystujemy adnotację @WebServlet do oznaczenia, że dana klasa będzie używana jako WebServlet o danej nazwie name oraz ścieżce URL urlPatterns
@WebServlet(name = "GroupsSearchServlet", urlPatterns = "/groups-search")
public class GroupsSearchServlet extends HttpServlet {
Zdefiniowana klasa rozszerza klasę abstrakcyjną HttpServlet, co wymusza przesłanianie przynajmniej jednej metody obsługującej żądanie HTTP. W naszym przypadku przesłaniamy tylko metody doGet oraz doPost obsługujące żądania Get oraz Post
Przesłanianie metody doGet i przekierowanie request Servleta do zasobu zdefiniowanego w wywołaniu metody getRequestDispatcher() (w tym przypadku do pliku jsp searchGroup.jsp) wygląda następująco
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getRequestDispatcher("searchGroups.jsp").forward(request, response);
}
Przesłaniamy metodę doPost, pobieramy parametr group_name z otrzymanego żądania. Jeśli nie podano żadnej nazwy grupy, to zostaje wykonane przeniesienie z powrotem do searchGroups.jsp z odpowiednim atrybutem wykorzystywanym do wyświetlenia informacji o wymaganiu podania nazwy grupy, którą chcemy wyszukać.
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String groupNameRegex = request.getParameter("group_name");
if (groupNameRegex.equals("")) {
request.setAttribute("no_group_name", true);
request.getRequestDispatcher("searchGroups.jsp").forward(request, response);
} else {
Jeśli podano szukaną nazwę grupy, to otwieramy sesję korzystając z metody getSession() z pliku HibernateUtils, gdzie jest zaimplementowana SessionFactory oraz rozpoczynamy transakcję na otwartej sesji.
Session session = HibernateUtils.getSession();
List<Group> groupsList = null;
Map<Group, String> groupsMap = new HashMap<>();
try {
Transaction tx = session.beginTransaction();
Uzyskujemy listę grup, które pasują do podanego parametru group_name korzystając z HQL
groupsList = session
.createQuery("from Group as group where group.name like :groupNameRegex", Group.class)
.setParameter("groupNameRegex", '%' + groupNameRegex + '%')
.getResultList();
Następnie przetwarzamy otrzymane dane z bazy sprawdzając, czy zalogowany użytkownik jest członkiem znalezionych grup, czy prosił już o dołączenie, lub czy może prosić o dołączenie. Przy tej czynności także korzystamy z HQL, aby otrzymać odpowiednie dane z tabel GroupMember oraz GroupRequest. Na końcu wywołujemy .commit() na transakcji.
for (Group group : groupsList) {
// check if user is already in the group
List<GroupMember> gm = session
.createQuery("from GroupMember as gm where gm.group.groupID=:gId and gm.user.userID=:uId",
GroupMember.class)
.setParameter("gId", group.getGroupID())
.setParameter("uId", LoggedUser.getUser().getUserID())
.getResultList();
if (gm != null && gm.size() == 1) {
groupsMap.put(group, "joined");
} else {
// check if user already requested joining the group
List<GroupRequest> gr = session
.createQuery("from GroupRequest as gr where gr.group.groupID=:gId and gr.user.userID=:uId",
GroupRequest.class)
.setParameter("gId", group.getGroupID())
.setParameter("uId", LoggedUser.getUser().getUserID())
.getResultList();
if (gr != null && gr.size() == 1) {
groupsMap.put(group, "requested");
} else {
groupsMap.put(group, "none");
}
}
}
tx.commit();
Jeśli w trakcie wykonywania transakcji pojawiły się błędy, to przekierowujemy żądanie Servleta do searchGroups.jsp wraz z odpowiednim atrybutem pozwalającym na wyświetlenie komunikatu o uzyskanym błędzie w aplikacji.
} catch (Exception ex) {
request.setAttribute("search_error", true);
request.getRequestDispatcher("searchGroups.jsp").forward(request, response);
ex.printStackTrace();
W każdym przypadku na końcu zamykamy sesję
} finally {
session.close();
}
Jeśli nie pojawiły się żadne błędy w czasie wykonywania transakcji, to przekierowujemy żądanie Servleta do searchGroups.jsp wraz z odpowiednim atrybutem do którego przypisujemy otrzymane rezultaty wyszukiwania
if (groupsList == null || groupsList.size() == 0) {
request.setAttribute("no_such_group", true);
}
request.setAttribute("groups", groupsMap);
request.getRequestDispatcher("searchGroups.jsp").forward(request, response);
Opis oparty na groupsSearch.jsp, który w aplikacji prezentuje się następująco:

Definiujemy ContentType oraz kodowanie strony, następnie importujemy używane prefixy z biblioteki Taglibs oraz Bootstrapa 5.0.1
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri = "http://java.sun.com/jsp/jstl/core" prefix = "c" %>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
Tworzymy formularz przekierowywujący do GroupsSearchServlet opisanego wyżej z parameterm group_name. Jest też możliwość powrotu do strony głównej profilu użytkownika naciskając na przycisk z napisem Return przekierowywującym do ReturnToMainPageServlet.
Tag if prefixu c pozwala przy pomocy atrybutu test przetestować podany warunek. (poniżej podano tylko jeden przykład użycia dla większej przejrzystości kodu)
requestScope to mapa dzięki której możemy odczytywać wartości przypisane request w Servletach za pomocą metody request.setAttribute(name, value)
<!DOCTYPE html>
<html>
<head>
<title>Group search</title>
</head>
<body>
<br/>
<div class="container-sm">
<div class="card">
<div class="card-header">
Search for a group
</div>
<div class="card-body">
<c:if test="${requestScope.group_leave_and_admin_change_success == true}">
<div class="alert alert-success" role="alert">
Left the group with admin changed
</div>
</c:if>
<form action="groups-search" method="post">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Group name" aria-label="Email"
aria-describedby="basic-addon1" name="group_name" id="group_name"
value="${param.group_name}">
</div>
<button type="submit" class="btn btn-outline-primary">Search</button>
<a href="returnToMainPage" class="btn btn-outline-primary">Return</a>
</form>
</div>
</div>
</div>
Jeśli zostało wykonane wyszukiwanie po podanej nazwie grupy, to do requestScope w Servletcie GroupsSearchServlets zostanie przypisana mapa z otrzymanymi grupami. Odczytujemy je korzystając z przefixu c oraz tagów if oraz forEach, gdzie tag forEach ma atrybut items wskazujący na mapę groups, po której będziemy iterować oraz var do którego zostaną przypisane kolejne wyniki iteracji po groups. Dzięki temu tworzymy strukturę HTML do wyświetlania danych pojedynczej grupy, ale będzie ona powtarzana tyle razy, ile grup otrzymaliśmy w wyniku wyszukiwania po nazwie grupy. Rozróżniamy grupy, które są kluczami w mapie groups po wartościach tych kluczów, czyli none -> użytkownik nie jest członkiem grupy oraz nie prosił o dołączenie, requested -> użytkownik nie jest członkiem grupy, ale prosił o dołączenie, joined -> użytkownik jest członkiem grupy
<c:if test="${requestScope.groups != null}">
<div class="container-sm">
<div class="card">
<div class="card-header">
Search results
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<c:forEach var="group" items="${requestScope.groups}">
<li class="list-group-item"><b>Group Name: </b> ${group.key.name}
<b>Description: </b> ${group.key.description}
<br/>
<c:if test="${group.value == 'none'}">
<form action="sendRequest" method="post">
<input type="hidden" name="group_id" value="${group.key.groupID}">
<button type="submit" class="btn btn-outline-primary">Join group</button>
</form>
</c:if>
<c:if test="${group.value == 'requested'}">
<form action="deleteRequest" method="post">
<input type="hidden" name="group_id" value="${group.key.groupID}">
<button type="submit" class="btn btn-outline-danger">Delete request</button>
</form>
</c:if>
<c:if test="${group.value == 'joined'}">
<form action="leaveGroup" method="post">
<input type="hidden" name="group_id" value="${group.key.groupID}">
<input type="hidden" name="returnPage" value="searchgroup.jsp">
<button type="submit" class="btn btn-outline-danger">Leave group</button>
</form>
</c:if>
</li>
</c:forEach>
</ul>
</div>
</div>
</div>
</c:if>
</body>
</html>
Stosowane do tworzenia klas będących potem mapowanych przez Hibernate do odpowiednich tabel bazy danych
Dla przykładu opisujemy klasę Mail
Adnotacja @Entity opisuje, że definiowana klasa jest encją bazy danych.
@Entity
public class Mail {
Adnotacja @Id oznacza klucz główny encji, a @GeneratedValue określa strategię generowania kolejnych wartości tego klucza głównego.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int mailID;
Definiujemy kolejne atrybuty encji podając przy pomocy adnotacji @Column, czy mogą one zawierać wartości null oraz jaka może być ich maksymalna długość
created potrzebuje specjalnych adnotacji, które określają odpowiedni typ tego atrybutu i sposób tworzenia jego wartości (@CreationTimestamp zapewnia, że dla każdego nowego obiektu tej encji wartość tego atrybutu to data utworzenia tego obiektu)
@Column(nullable = false, length = 1000)
private String message;
@Column(nullable = false, length = 32)
private String title;
@Column(nullable = false)
@Temporal(TemporalType.TIMESTAMP)
@CreationTimestamp
private Date created;
Adnotacja @ManyToOne określa relację pomiędzy Mail a Group, gdzie jedna grupa z tabeli Group może mieć przypisane wiele maili z tabeli Mail. Dodatkowo zastosowana konfiguracja adnotacji @OnDelete pozwala na kaskadowe usuwanie encji Mail powiązanych z usuwaną encją z tabeli Group
@ManyToOne
@JoinColumn(name = "groupID", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Group group;
Tworzymy wymagany konstruktor bezparametrowy oraz drugi z wybranymi parametrami
public Mail() {
}
public Mail(String message, String title, Group group) {
this.message = message;
this.title = title;
this.group = group;
}
Tworzymy odpowiednie metody get oraz set ułatwiające operacje na encjach.
public int getMailID() {
return mailID;
}
public void setMailID(int mailID) {
this.mailID = mailID;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Group getGroup() {
return group;
}
public int getGroupId() {
return this.group.getGroupID();
}
public void setGroup(Group group) {
this.group = group;
}
public Date getCreated() {
return created;
}
public String getCreatedFormatted() {
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a");
return formatter.format(created);
}

