Compare commits

...

7 Commits

21 changed files with 926 additions and 116 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
POSTGRES_USER='postgres'
POSTGRES_PASSWORD='123456'
POSTGRES_PORT=5434

16
compose.yml Normal file
View File

@ -0,0 +1,16 @@
version: '3.9'
name: tinder-clone
services:
postgres:
container_name: tinder-clone-postgres
image: postgres:15-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "${POSTGRES_PORT}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:

BIN
lib/postgresql-42.7.10.jar Normal file

Binary file not shown.

0
media/profiles/4 Normal file
View File

View File

@ -4,20 +4,178 @@
<head> <head>
<title>Charm Profiles</title> <title>Charm Profiles</title>
<%@ include file="style.html" %> <%@ include file="style.html" %>
<c:url var="baseUrl" value="${requestScope['javax.servlet.forward.servlet_path']}">
<c:param name="emailStartWith" value="${filter.emailStartWith}"/>
<c:param name="nameStartWith" value="${filter.nameStartWith}"/>
<c:param name="surnameStartWith" value="${filter.surnameStartWith}"/>
<c:param name="status" value="${filter.status}"/>
<c:param name="gteAge" value="${filter.gteAge}"/>
<c:param name="ltAge" value="${filter.ltAge}"/>
<c:param name="sort" value="id"/>
</c:url>
<c:url var="sortUrlWithId" value="${baseUrl}">
<c:param name="sort" value="id"/>
</c:url>
<c:url var="sortUrlWithEmail" value="${baseUrl}">
<c:param name="sort" value="email"/>
</c:url>
<c:url var="sortUrlWithName" value="${baseUrl}">
<c:param name="sort" value="name"/>
</c:url>
<c:url var="sortUrlWithSurname" value="${baseUrl}">
<c:param name="sort" value="surname"/>
</c:url>
<c:url var="sortUrlWithAge" value="${baseUrl}">
<c:param name="sort" value="birth_date"/>
</c:url>
<c:url var="sortUrlWithStatus" value="${baseUrl}">
<c:param name="sort" value="status"/>
</c:url>
<c:url var="sortUrlWithRole" value="${baseUrl}">
<c:param name="sort" value="role"/>
</c:url>
</head> </head>
<body> <body>
<%@ include file="header.jsp" %> <%@ include file="header.jsp" %>
<div> <div>
<form action="/profiles" method="get">
<input type="hidden" name="sort" value="${filter.sort}">
<table>
<tr class="hiddenRow">
<td>
<input type="image" src="content/app/img/filter.png" width="75" alt="submit" class="icon"/>
</td>
<td>
<table>
<tr class="hiddenRow">
<td>${wordBundle.getWord("email")}</td>
<td><input type="text" name="emailStartWith" value="${filter.emailStartWith}"
class="filterInput"></td>
</tr>
<tr class="hiddenRow">
<td>${wordBundle.getWord("name")}</td>
<td><input type="text" name="nameStartWith" value="${filter.nameStartWith}"
class="filterInput"></td>
</tr>
</table>
</td>
<td>
<table>
<tr class="hiddenRow">
<td>${wordBundle.getWord("surname")}</td>
<td><input type="text" name="surnameStartWith" value="${filter.surnameStartWith}"
class="filterInput"></td>
</tr>
<tr class="hiddenRow">
<td>${wordBundle.getWord("status")}</td>
<td>
<select name="status" class="filterInput">
<option selected value></option>
<c:forEach var="statusVar" items="${applicationScope.statuses}">
<c:if test="${statusVar == filter.status}">
<option value="${statusVar}" selected>
${wordBundle.getWord(statusVar)}
</option>
</c:if>
<c:if test="${statusVar != filter.status}">
<option value="${statusVar}">${wordBundle.getWord(statusVar)}
</option>
</c:if>
</c:forEach>
</select>
</td>
</tr>
</table>
</td>
<td>
<table>
<tr class="hiddenRow">
<td>${wordBundle.getWord("age")}</td>
<td><input type="number" name="gteAge" min="18" max="100" value="${filter.gteAge}"
style="width: 50px;"></td>
<td>-</td>
<td><input type="number" name="ltAge" min="18" max="100" value="${filter.ltAge}"
style="width: 50px;"></td>
</tr>
</table>
</td>
</tr>
</table>
</form>
<table> <table>
<tr> <tr>
<td><h3>id</h3></td> <td>
<td><h3>${requestScope.wordBundle.getWord("email")}</h3></td> <a href="${sortUrlWithId}" class="hiddenLink">
<td><h3>${requestScope.wordBundle.getWord("name")}</h3></td> <h3>
<td><h3>${requestScope.wordBundle.getWord("surname")}</h3></td> id
<td><h3>${requestScope.wordBundle.getWord("age")}</h3></td> <c:if test="${filter.sort == 'id' or filter.sort == null}">
<td><h3>${requestScope.wordBundle.getWord("status")}</h3></td> ^
</c:if>
</h3>
</a>
</td>
<td>
<a href="${sortUrlWithEmail}" class="hiddenLink">
<h3>
${wordBundle.getWord("email")}
<c:if test="${filter.sort == 'email'}">
^
</c:if>
</h3>
</a>
</td>
<td>
<a href="${sortUrlWithName}" class="hiddenLink">
<h3>
${wordBundle.getWord("name")}
<c:if test="${filter.sort == 'name'}">
^
</c:if>
</h3>
</a>
</td>
<td>
<a href="${sortUrlWithSurname}" class="hiddenLink">
<h3>
${wordBundle.getWord("surname")}
<c:if test="${filter.sort == 'surname'}">
^
</c:if>
</h3>
</a>
</td>
<td>
<a href="${sortUrlWithAge}" class="hiddenLink">
<h3>
${wordBundle.getWord("age")}
<c:if test="${filter.sort == 'birth_date'}">
^
</c:if>
</h3>
</a>
</td>
<td>
<a href="${sortUrlWithStatus}" class="hiddenLink">
<h3>
${wordBundle.getWord("status")}
<c:if test="${filter.sort == 'status'}">
^
</c:if>
</h3>
</a>
</td>
<td>
<a href="${sortUrlWithRole}" class="hiddenLink">
<h3>
${wordBundle.getWord("role")}
<c:if test="${filter.sort == 'role'}">
^
</c:if>
</h3>
</a>
</td>
</tr> </tr>
<c:forEach var="profile" items="${requestScope.profiles}"> <c:forEach var="profile" items="${profiles}">
<tr> <tr>
<td><h4>${profile.id}</h4></td> <td><h4>${profile.id}</h4></td>
<td><h4>${profile.email}</h4></td> <td><h4>${profile.email}</h4></td>
@ -25,20 +183,28 @@
<td><h4>${profile.surname}</h4></td> <td><h4>${profile.surname}</h4></td>
<td><h4>${profile.age}</h4></td> <td><h4>${profile.age}</h4></td>
<td> <td>
<form action="/profile" method="post"> <form action="/profile" method="post" enctype="multipart/form-data">
<input type="hidden" name="_method" value="put"/> <input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="${profile.id}"> <input type="hidden" name="id" value="${profile.id}">
<select name="status"> <select name="status" class="filterInput">
<option value="${profile.status}" selected hidden> <c:forEach var="statusVar" items="${applicationScope.statuses}">
${requestScope.wordBundle.getWord(profile.status)} <c:if test="${statusVar == profile.status}">
<option value="${statusVar}" selected>
${wordBundle.getWord(statusVar)}
</option> </option>
<c:forEach var="status" items="${applicationScope.statuses}"> </c:if>
<option value="${status}">${requestScope.wordBundle.getWord(status)}</option> <c:if test="${statusVar != profile.status}">
<option value="${statusVar}">${wordBundle.getWord(statusVar)}
</option>
</c:if>
</c:forEach> </c:forEach>
</select> </select>
<button type="submit">${requestScope.wordBundle.getWord("save")}</button> <c:if test="${profile.role != 'ADMIN'}">
<button type="submit">${wordBundle.getWord("save")}</button>
</c:if>
</form> </form>
</td> </td>
<td><h4>${profile.role}</h4></td>
</tr> </tr>
</c:forEach> </c:forEach>
</table> </table>

View File

@ -0,0 +1,7 @@
app.datasource.url=jdbc:postgresql://localhost:5434/charm_repository
app.datasource.username=postgres
app.datasource.password=123456
app.datasource.driver=org.postgresql.Driver
app.datasource.fetch-size=50
app.datasource.max-rows=100
app.datasource.query-timeout=5

View File

@ -10,8 +10,11 @@ import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.dto.ProfileGetDto; import ru.charm.back.dto.ProfileGetDto;
import ru.charm.back.dto.ProfileUpdateDto; import ru.charm.back.dto.ProfileUpdateDto;
import ru.charm.back.mapper.RequestToProfileFilterMapper;
import ru.charm.back.mapper.RequestToProfileUpdateDtoMapper; import ru.charm.back.mapper.RequestToProfileUpdateDtoMapper;
import ru.charm.back.service.ProfileService; import ru.charm.back.service.ProfileService;
@ -20,13 +23,17 @@ import java.io.OutputStream;
import java.util.Optional; import java.util.Optional;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static ru.charm.back.utils.UrlUtils.PROFILE_URL;
@WebServlet("/profile/*") @WebServlet(PROFILE_URL + "/*")
@MultipartConfig @MultipartConfig
@Slf4j
public class ProfileController extends HttpServlet { public class ProfileController extends HttpServlet {
private final ProfileService service = ProfileService.getInstance(); private final ProfileService service = ProfileService.getInstance();
private final RequestToProfileUpdateDtoMapper requestToProfileUpdateDtoMapper = RequestToProfileUpdateDtoMapper.getInstance(); private final RequestToProfileUpdateDtoMapper requestToProfileUpdateDtoMapper = RequestToProfileUpdateDtoMapper.getInstance();
private final RequestToProfileFilterMapper requestToProfileFilterMapper = RequestToProfileFilterMapper.getInstance();
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
@ -66,7 +73,8 @@ public class ProfileController extends HttpServlet {
resp.sendError(SC_NOT_FOUND); resp.sendError(SC_NOT_FOUND);
} }
} else { } else {
req.setAttribute("profiles", service.findAll()); ProfileFilter filter = requestToProfileFilterMapper.map(req);
req.setAttribute("profiles", service.findAll(filter));
req.getRequestDispatcher("/WEB-INF/jsp/profiles.jsp").forward(req, resp); req.getRequestDispatcher("/WEB-INF/jsp/profiles.jsp").forward(req, resp);
} }
} }

View File

@ -0,0 +1,33 @@
package ru.charm.back.controller;
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 lombok.extern.slf4j.Slf4j;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.mapper.RequestToProfileFilterMapper;
import ru.charm.back.service.ProfileService;
import java.io.IOException;
import static ru.charm.back.utils.UrlUtils.PROFILES_URL;
import static ru.charm.back.utils.UrlUtils.getJspPath;
@WebServlet(PROFILES_URL)
@Slf4j
public class ProfilesController extends HttpServlet {
private final ProfileService service = ProfileService.getInstance();
private final RequestToProfileFilterMapper requestToProfileFilterMapper = RequestToProfileFilterMapper.getInstance();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
ProfileFilter filter = requestToProfileFilterMapper.map(req);
req.setAttribute("profiles", service.findAll(filter));
req.setAttribute("filter", filter);
req.getRequestDispatcher(getJspPath(PROFILES_URL)).forward(req, res);
}
}

View File

@ -9,7 +9,9 @@ import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.dto.ProfileGetDto; import ru.charm.back.dto.ProfileGetDto;
import ru.charm.back.mapper.RequestToProfileFilterMapper;
import ru.charm.back.service.ProfileService; import ru.charm.back.service.ProfileService;
import java.io.IOException; import java.io.IOException;
@ -23,6 +25,8 @@ import static ru.charm.back.utils.UrlUtils.REST_URL;
@MultipartConfig @MultipartConfig
public class ProfileController extends HttpServlet { public class ProfileController extends HttpServlet {
private final ProfileService service = ProfileService.getInstance(); private final ProfileService service = ProfileService.getInstance();
private final RequestToProfileFilterMapper requestToProfileFilterMapper = RequestToProfileFilterMapper.getInstance();
private final ObjectMapper objectMapper = JsonMapper.builder() private final ObjectMapper objectMapper = JsonMapper.builder()
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
@ -45,7 +49,8 @@ public class ProfileController extends HttpServlet {
resp.sendError(SC_NOT_FOUND); resp.sendError(SC_NOT_FOUND);
} }
} else { } else {
objectMapper.writeValue(writer, service.findAll()); ProfileFilter filter = requestToProfileFilterMapper.map(req);
objectMapper.writeValue(writer, service.findAll(filter));
} }
} }
} }

View File

@ -1,92 +1,255 @@
package ru.charm.back.dao; package ru.charm.back.dao;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.dto.ProfileSelectQueryBuilder;
import ru.charm.back.dto.ProfileUpdateQueryBuilder;
import ru.charm.back.dto.Query;
import ru.charm.back.model.Gender; import ru.charm.back.model.Gender;
import ru.charm.back.model.Profile; import ru.charm.back.model.Profile;
import ru.charm.back.model.Role; import ru.charm.back.model.Role;
import ru.charm.back.model.Status; import ru.charm.back.model.Status;
import ru.charm.back.utils.ConfigFileUtils;
import ru.charm.back.utils.ConnectionManager;
import java.time.LocalDate; import java.sql.*;
import java.util.ArrayList; import java.sql.Date;
import java.util.List; import java.util.*;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import static ru.charm.back.utils.ConnectionManager.getConnection;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ProfileDao { public class ProfileDao {
public static final String INSERT = "INSERT INTO profile (email, password) VALUES (?, ?)";
private static final ProfileDao INSTANCE = new ProfileDao(); private static final ProfileDao INSTANCE = new ProfileDao();
private Set<String> validColumns;
private final AtomicLong idStorage; @SneakyThrows
private final ConcurrentHashMap<Long, Profile> storage;
private ProfileDao() {
this.storage = new ConcurrentHashMap<>();
Profile profile = new Profile();
profile.setId(1L);
profile.setEmail("ivanov@mail.ru");
profile.setPassword("123");
profile.setName("Ivan");
profile.setSurname("Ivanov");
profile.setBirthDate(LocalDate.parse("2001-12-03"));
profile.setAbout("I am QA");
profile.setGender(Gender.MALE);
profile.setStatus(Status.ACTIVE);
profile.setRole(Role.ADMIN);
this.storage.put(1L, profile);
Profile profile1 = new Profile();
profile1.setId(2L);
profile1.setEmail("sidorova@mail.ru");
profile1.setPassword("456");
profile1.setName("Elena");
profile1.setSurname("Sidorova");
profile1.setBirthDate(LocalDate.parse("1999-09-01"));
profile1.setAbout("I am Java Dev");
profile1.setGender(Gender.FEMALE);
profile1.setStatus(Status.INACTIVE);
profile1.setRole(Role.USER);
this.storage.put(2L, profile1);
this.idStorage = new AtomicLong(3L);
}
public static ProfileDao getInstance() { public static ProfileDao getInstance() {
return INSTANCE; return INSTANCE;
} }
public Profile save(Profile profile) { public Profile save(Profile profile) {
profile.setId(idStorage.getAndIncrement()); try (
storage.put(profile.getId(), profile); Connection connection = getConnection();
return profile; PreparedStatement statement = connection.prepareStatement(INSERT, Statement.RETURN_GENERATED_KEYS);
) {
statement.setString(1, profile.getEmail());
statement.setString(2, profile.getPassword());
int insertCount = statement.executeUpdate();
ResultSet res = statement.getGeneratedKeys();
if (res.next()) {
profile.setId(res.getLong("id"));
} }
return profile;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Optional<Profile> findById(Long id) { public Optional<Profile> findById(Long id) {
if (id == null) return Optional.empty(); Query query = new ProfileSelectQueryBuilder().addIdFilter(id).build();
return Optional.ofNullable(storage.get(id));
try (
Connection connection = getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
ResultSet res = statement.executeQuery();
Profile profile = null;
if (res.next()) {
profile = mapToProfile(res);
} }
public List<Profile> findAll() { return Optional.ofNullable(profile);
return new ArrayList<>(storage.values());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public List<Profile> findAll(ProfileFilter filter) {
Query query = new ProfileSelectQueryBuilder()
.addEmailStartWithFilter(filter.getEmailStartWith())
.addNameStartWith(filter.getNameStartWith())
.addSurnameStartWith(filter.getSurnameStartWith())
.addStatus(filter.getStatus())
.addLTAge(filter.getLtAge())
.addGTEAge(filter.getGteAge())
.build(getValidSort(filter.getSort()));
try (
Connection connection = ConnectionManager.getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
statement.setFetchSize(ConnectionManager.FETCH_SIZE);
statement.setMaxRows(ConnectionManager.MAX_ROWS);
statement.setQueryTimeout(ConnectionManager.QUERY_TIMEOUT);
ResultSet res = statement.executeQuery();
List<Profile> profiles = new ArrayList<>();
while (res.next()) {
profiles.add(mapToProfile(res));
}
return profiles;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String getValidSort(String sort) {
if (validColumns == null) {
validColumns = new HashSet<>();
try (Connection conn = getConnection();) {
ResultSet rs = conn.getMetaData().getColumns(null, null, "profile", null);
while(rs.next()) {
String comment = rs.getString("REMARKS");
if("sortable".equals(comment)) {
validColumns.add(rs.getString("COLUMN_NAME"));
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (validColumns.contains(sort)) {
return sort;
} else {
return "id";
}
} }
public void update(Profile profile) { public void update(Profile profile) {
Long id = profile.getId(); Query query = new ProfileUpdateQueryBuilder()
if (id == null) return; .addEmail(profile.getEmail())
storage.put(id, profile); .addPassword(profile.getPassword())
.addName(profile.getName())
.addSurname(profile.getSurname())
.addBirthDate(profile.getBirthDate())
.addAbout(profile.getAbout())
.addGender(profile.getGender())
.addStatus(profile.getStatus())
.addPhoto(profile.getPhoto())
.build(profile.getId());
try (Connection conn = ConnectionManager.getConnection();
PreparedStatement stmt = ConnectionManager.getPreparedStmt(conn, query)) {
stmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
} }
public boolean delete(Long id) { public boolean delete(Long id) {
if (id == null) return false; String sql = "DELETE FROM profile WHERE id = ?";
return storage.remove(id) != null; try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
int deleteCount = stmt.executeUpdate();
log.debug("Delete count: {}", deleteCount);
return deleteCount > 0;
} catch (SQLException e) {
throw new RuntimeException(e);
}
} }
public Set<String> getAllEmails() { public Set<String> getAllEmails() {
return storage.values().stream().map(Profile::getEmail).collect(Collectors.toSet()); try (
Connection connection = getConnection();
Statement statement = connection.createStatement();
) {
String sql = "SELECT email FROM profile" ;
ResultSet res = statement.executeQuery(sql);
ArrayList<String> emails = new ArrayList<>();
while (res.next()) {
emails.add(res.getString("email"));
}
return new HashSet<>(emails);
} catch (SQLException e) {
throw new RuntimeException(e);
}
} }
public Optional<Profile> findByEmail(String email) { public Optional<Profile> findByEmail(String email) {
if (email == null) return Optional.empty(); Query query = new ProfileSelectQueryBuilder().addEmailFilter(email).build();
try (
Connection connection = ConnectionManager.getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
statement.setString(1, email);
ResultSet res = statement.executeQuery();
Profile profile = null;
if (res.next()) {
profile = mapToProfile(res);
}
return Optional.ofNullable(profile);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Boolean existsByEmail(String email) {
Query query = new ProfileSelectQueryBuilder().addEmailFilter(email).build();
try (
Connection connection = ConnectionManager.getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
ResultSet res = statement.executeQuery();
return res.next();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Profile mapToProfile(ResultSet rs) throws SQLException {
Profile result = new Profile();
result.setId(rs.getLong("id"));
result.setEmail(rs.getString("email"));
result.setPassword(rs.getString("password"));
result.setName(rs.getString("name"));
result.setSurname(rs.getString("surname"));
Date birthDate = rs.getDate("birth_date");
if (birthDate != null) {
result.setBirthDate(birthDate.toLocalDate());
}
result.setAbout(rs.getString("about"));
String gender = rs.getString("gender");
if (gender != null) {
result.setGender(Gender.valueOf(gender));
}
result.setPhoto(rs.getString("photo"));
String status = rs.getString("status");
if (status != null) {
result.setStatus(Status.valueOf(status));
}
String role = rs.getString("role");
if (role != null) {
result.setRole(Role.valueOf(role));
}
return result;
return storage.values().stream().filter(profile -> profile.getEmail().equals(email)).findFirst();
} }
} }

View File

@ -0,0 +1,18 @@
package ru.charm.back.dto;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import ru.charm.back.model.Status;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ProfileFilter {
String emailStartWith;
String nameStartWith;
String surnameStartWith;
Integer ltAge;
Integer gteAge;
Status status;
String sort;
}

View File

@ -0,0 +1,120 @@
package ru.charm.back.dto;
import ru.charm.back.model.Status;
import java.sql.Date;
import java.util.ArrayList;
import java.util.List;
import static ru.charm.back.utils.DateTimeUtils.getPastDate;
public class ProfileSelectQueryBuilder {
//language=POSTGRES-PSQL
public static final String SELECT_BASE = """
SELECT id, email, password, "name", surname, birth_date, about, gender, photo, status, role
FROM profile
WHERE '' = ''
""";
private final StringBuilder sb;
private final List<Object> args;
public ProfileSelectQueryBuilder() {
this.sb = new StringBuilder(SELECT_BASE);
this.args = new ArrayList<>();
}
public ProfileSelectQueryBuilder addIdFilter(Long id) {
if (id == null) {
return this;
}
sb.append(" AND id = ?");
args.add(id);
return this;
}
public ProfileSelectQueryBuilder addEmailFilter(String email) {
if (email == null) {
return this;
}
sb.append(" AND email = ?");
args.add(email);
return this;
}
public ProfileSelectQueryBuilder addPasswordFilter(String password) {
if (password == null) {
return this;
}
sb.append(" AND password = ?");
args.add(password);
return this;
}
public ProfileSelectQueryBuilder addEmailStartWithFilter(String emailStartWith) {
if (emailStartWith == null) {
return this;
}
sb.append(" AND email like ?");
args.add(emailStartWith + "%");
return this;
}
public ProfileSelectQueryBuilder addNameStartWith(String nameStartWith) {
if (nameStartWith == null) {
return this;
}
sb.append(" AND name like ?");
args.add(nameStartWith + "%");
return this;
}
public ProfileSelectQueryBuilder addSurnameStartWith(String surnameStartWith) {
if (surnameStartWith == null) {
return this;
}
sb.append(" AND surname like ?");
args.add(surnameStartWith + "%");
return this;
}
public ProfileSelectQueryBuilder addLTAge(Integer ltAge) {
if (ltAge == null) {
return this;
}
Date gtDate = getPastDate(ltAge);
sb.append(" AND birth_date > ?");
args.add(gtDate);
return this;
}
public ProfileSelectQueryBuilder addGTEAge(Integer gteAge) {
if (gteAge == null) {
return this;
}
Date lteDate = getPastDate(gteAge);
sb.append(" AND birth_date <= ?");
args.add(lteDate);
return this;
}
public ProfileSelectQueryBuilder addStatus(Status status) {
if (status == null) {
return this;
}
sb.append(" AND status = ?");
args.add(status.toString());
return this;
}
public Query build() {
return new Query(sb.toString(), args);
}
public Query build(String sort) {
sb.append(" ORDER BY ").append(sort).append(" ASC");
return new Query(sb.toString(), args);
}
}

View File

@ -0,0 +1,109 @@
package ru.charm.back.dto;
import ru.charm.back.model.Gender;
import ru.charm.back.model.Status;
import java.sql.Date;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class ProfileUpdateQueryBuilder {
//language=POSTGRES-PSQL
public static final String UPDATE_BASE = "UPDATE profile SET id = id";
private final StringBuilder sb;
private final List<Object> args;
public ProfileUpdateQueryBuilder() {
this.sb = new StringBuilder(UPDATE_BASE);
this.args = new ArrayList<>();
}
public ProfileUpdateQueryBuilder addEmail(String email) {
if (email == null) {
return this;
}
sb.append(", email = ?");
args.add(email);
return this;
}
public ProfileUpdateQueryBuilder addPassword(String password) {
if (password == null) {
return this;
}
sb.append(", password = ?");
args.add(password);
return this;
}
public ProfileUpdateQueryBuilder addName(String name) {
if (name == null) {
return this;
}
sb.append(", name = ?");
args.add(name);
return this;
}
public ProfileUpdateQueryBuilder addSurname(String surname) {
if (surname == null) {
return this;
}
sb.append(", surname = ?");
args.add(surname);
return this;
}
public ProfileUpdateQueryBuilder addBirthDate(LocalDate birthDate) {
if (birthDate == null) {
return this;
}
sb.append(", birth_date = ?");
args.add(Date.valueOf(birthDate));
return this;
}
public ProfileUpdateQueryBuilder addAbout(String about) {
if (about == null) {
return this;
}
sb.append(", about = ?");
args.add(about);
return this;
}
public ProfileUpdateQueryBuilder addGender(Gender gender) {
if (gender == null) {
return this;
}
sb.append(", gender = ?");
args.add(gender.toString());
return this;
}
public ProfileUpdateQueryBuilder addStatus(Status status) {
if (status == null) {
return this;
}
sb.append(", status = ?");
args.add(status.toString());
return this;
}
public ProfileUpdateQueryBuilder addPhoto(String photo) {
if (photo == null) {
return this;
}
sb.append(", photo = ?");
args.add(photo);
return this;
}
public Query build(Long id) {
sb.append(" WHERE id = ?");
args.add(id);
return new Query(sb.toString(), args);
}
}

View File

@ -0,0 +1,6 @@
package ru.charm.back.dto;
import java.util.List;
public record Query(String sql, List<Object> args) {
}

View File

@ -25,8 +25,6 @@ public class RegistrationDtoToProfileMapper implements Mapper<RegistrationDto, P
public Profile map(RegistrationDto dto, Profile profile) { public Profile map(RegistrationDto dto, Profile profile) {
profile.setEmail(dto.getEmail()); profile.setEmail(dto.getEmail());
profile.setPassword(dto.getPassword()); profile.setPassword(dto.getPassword());
profile.setStatus(Status.INACTIVE);
profile.setRole(Role.USER);
return profile; return profile;
} }
} }

View File

@ -0,0 +1,58 @@
package ru.charm.back.mapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.model.Status;
import static ru.charm.back.utils.StringUtils.isBlank;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RequestToProfileFilterMapper implements Mapper<HttpServletRequest, ProfileFilter> {
private static final RequestToProfileFilterMapper INSTANCE = new RequestToProfileFilterMapper();
public static RequestToProfileFilterMapper getInstance() {
return INSTANCE;
}
@Override
public ProfileFilter map(HttpServletRequest req) {
return map(req, new ProfileFilter());
}
@Override
public ProfileFilter map(HttpServletRequest req, ProfileFilter filter) {
String emailStartWithArg = req.getParameter("emailStartWith");
String emailStartWith = isBlank(emailStartWithArg) ? null : emailStartWithArg;
filter.setEmailStartWith(emailStartWith);
String nameStartWithArg = req.getParameter("nameStartWith");
String nameStartWith = isBlank(nameStartWithArg) ? null : nameStartWithArg;
filter.setNameStartWith(nameStartWith);
String surnameStartWithArg = req.getParameter("surnameStartWith");
String surnameStartWith = isBlank(surnameStartWithArg) ? null : surnameStartWithArg;
filter.setSurnameStartWith(surnameStartWith);
String ltAgeArg = req.getParameter("ltAge");
Integer ltAge = isBlank(ltAgeArg) ? null : Integer.parseInt(ltAgeArg);
filter.setLtAge(ltAge);
String gteAgeArg = req.getParameter("gteAge");
Integer gteAge = isBlank(gteAgeArg) ? null : Integer.parseInt(gteAgeArg);
filter.setGteAge(gteAge);
String statusArg = req.getParameter("status");
Status status = isBlank(statusArg) ? null : Status.valueOf(statusArg);
filter.setStatus(status);
String sortArg = req.getParameter("sort");
String sort = isBlank(sortArg) ? "id" : sortArg;
filter.setSort(sort);
return filter;
}
}

View File

@ -4,10 +4,7 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import ru.charm.back.dao.ProfileDao; import ru.charm.back.dao.ProfileDao;
import ru.charm.back.dto.LoginDto; import ru.charm.back.dto.*;
import ru.charm.back.dto.ProfileGetDto;
import ru.charm.back.dto.ProfileUpdateDto;
import ru.charm.back.dto.RegistrationDto;
import ru.charm.back.mapper.ProfileToProfileGetDtoMapper; import ru.charm.back.mapper.ProfileToProfileGetDtoMapper;
import ru.charm.back.mapper.ProfileUpdateDtoToProfileMapper; import ru.charm.back.mapper.ProfileUpdateDtoToProfileMapper;
import ru.charm.back.mapper.RegistrationDtoToProfileMapper; import ru.charm.back.mapper.RegistrationDtoToProfileMapper;
@ -46,8 +43,8 @@ public class ProfileService {
return dao.findById(id).map(profileToProfileGetDtoMapper::map); return dao.findById(id).map(profileToProfileGetDtoMapper::map);
} }
public List<ProfileGetDto> findAll() { public List<ProfileGetDto> findAll(ProfileFilter filter) {
return dao.findAll().stream().map(profileToProfileGetDtoMapper::map).toList(); return dao.findAll(filter).stream().map(profileToProfileGetDtoMapper::map).toList();
} }
@SneakyThrows @SneakyThrows

View File

@ -0,0 +1,29 @@
package ru.charm.back.utils;
import lombok.experimental.UtilityClass;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
@UtilityClass
public class ConfigFileUtils {
private static final Properties CONFIG = new Properties();
static {
loadConfig();
}
public static String get(String key) {
return CONFIG.getProperty(key);
}
private static void loadConfig() {
try (InputStream resourceAsStream = ConfigFileUtils.class.getClassLoader().getResourceAsStream("application.properties")) {
CONFIG.load(resourceAsStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,48 @@
package ru.charm.back.utils;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import ru.charm.back.dto.Query;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
@UtilityClass
public class ConnectionManager {
public static final String URL = ConfigFileUtils.get("app.datasource.url");
public static final String USER = ConfigFileUtils.get("app.datasource.username");
public static final String PASSWORD = ConfigFileUtils.get("app.datasource.password");
public static final String DRIVER = ConfigFileUtils.get("app.datasource.driver");
private static final String FETCH_SIZE_STR = ConfigFileUtils.get("app.datasource.fetch-size");
public static final int FETCH_SIZE = Integer.parseInt(FETCH_SIZE_STR != null ? FETCH_SIZE_STR : "100");
private static final String MAX_ROWS_STR = ConfigFileUtils.get("app.datasource.max-rows");
public static final int MAX_ROWS = Integer.parseInt(MAX_ROWS_STR != null ? MAX_ROWS_STR : "1000");
private static final String QUERY_TIMEOUT_STR = ConfigFileUtils.get("app.datasource.query-timeout");
public static final int QUERY_TIMEOUT = Integer.parseInt(QUERY_TIMEOUT_STR != null ? QUERY_TIMEOUT_STR : "10");
static {
if (DRIVER != null) {
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
public static PreparedStatement getPreparedStmt(Connection conn, Query query) throws SQLException {
PreparedStatement stmt = conn.prepareStatement(query.sql());
List<Object> args = query.args();
for (int i = 0; i < args.size(); i++) {
stmt.setObject(i + 1, args.get(i));
}
return stmt;
}
}

View File

@ -0,0 +1,25 @@
package ru.charm.back.utils;
import lombok.experimental.UtilityClass;
import java.sql.Date;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
@UtilityClass
public class DateTimeUtils {
public static int getAge(LocalDate birthDate) {
return Math.toIntExact(ChronoUnit.YEARS.between(birthDate, LocalDate.now()));
}
public static Date getPastDate(int age) {
return Date.valueOf(LocalDate.now().minusYears(age));
}
public static boolean isValidAge(LocalDate birthDate) {
if (birthDate == null) return false;
int age = getAge(birthDate);
return age > 18 && age < 100;
}
}

View File

@ -8,15 +8,19 @@ import java.util.Set;
public class UrlUtils { public class UrlUtils {
public static final String PROFILE_URL = "/profile"; public static final String PROFILE_URL = "/profile";
public static final String PROFILES_URL = "/profiles";
public static final String CREDENTIALS_URL = "/credentials"; public static final String CREDENTIALS_URL = "/credentials";
public static final String LOGIN_URL = "/login"; public static final String LOGIN_URL = "/login";
public static final String LOGOUT_URL = "/logout"; public static final String LOGOUT_URL = "/logout";
public static final String REGISTRATION_URL = "/registration"; public static final String REGISTRATION_URL = "/registration";
public static final String LANG_URL = "/lang"; public static final String LANG_URL = "/lang";
public static final String CONTENT_URL = "/content"; public static final String CONTENT_URL = "/content";
public static final String BASE_CONTENT_PATH = "/Users/andrey.s.eliseev/Downloads"; public static final String PDF_URL = "/pdf";
public static final String REST_URL = "/api/v1";
public static final String LOGIN_REST_URL = REST_URL + LOGIN_URL;
public static final String BASE_CONTENT_PATH = ConfigFileUtils.get("app.base-content-path");
public static final Set<String> PRIVATE_PATHS = Set.of(PROFILE_URL, CREDENTIALS_URL); public static final Set<String> PRIVATE_PATHS = Set.of(PROFILE_URL, PROFILES_URL, CREDENTIALS_URL, REST_URL);
public static final Set<String> ENTRY_PATHS = Set.of(LOGIN_URL, REGISTRATION_URL); public static final Set<String> ENTRY_PATHS = Set.of(LOGIN_URL, REGISTRATION_URL);
@ -24,9 +28,6 @@ public class UrlUtils {
return "/WEB-INF/jsp" + url + ".jsp"; return "/WEB-INF/jsp" + url + ".jsp";
} }
public static final String REST_URL = "/api/v1";
public static final String LOGIN_REST_URL = REST_URL + LOGIN_URL;
public static String getProfilePhotoPath(Long id, String fileName) { public static String getProfilePhotoPath(Long id, String fileName) {
return "/profiles/" + id + "/" + fileName; return "/profiles/" + id + "/" + fileName;
} }