diff --git a/pom.xml b/pom.xml
index 040f679..a61614b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -115,6 +115,12 @@
7.0.0
+
+ org.json
+ json
+ 20231013
+
+
diff --git a/src/main/java/back/activitymanager/api/LocalAdaptorApi.java b/src/main/java/back/activitymanager/api/LocalAdaptorApi.java
new file mode 100644
index 0000000..989bb4d
--- /dev/null
+++ b/src/main/java/back/activitymanager/api/LocalAdaptorApi.java
@@ -0,0 +1,15 @@
+package back.activitymanager.api;
+
+import java.io.IOException;
+
+public class LocalAdaptorApi {
+
+ public static String getLocalNameByLatLon(double lat, double lng) {
+ return LocationApi.getLocalNameByLatLon(lat, lng);
+ }
+
+ public static boolean isEtLocal(double myLat, double myLng, String districtName)
+ throws IOException {
+ return LocationApi.isEtLocal(myLat, myLng, districtName);
+ }
+}
diff --git a/src/main/java/back/activitymanager/api/LocationApi.java b/src/main/java/back/activitymanager/api/LocationApi.java
new file mode 100644
index 0000000..8a868fd
--- /dev/null
+++ b/src/main/java/back/activitymanager/api/LocationApi.java
@@ -0,0 +1,104 @@
+package back.activitymanager.api;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class LocationApi {
+
+ private static final int EARTH_RADIUS_KM = 6371;
+ private static final String CITY = "Київ";
+ private static final String COUNTRY = "Україна";
+ private static final String SUBURB = "Центр";
+ private static final double DEFAULT_RADIUS_KM = 5;
+ private static final String USER_AGENT = "YourApp";
+ private static final String NOMINATIM_URL = "https://nominatim.openstreetmap.org/";
+
+ public static boolean isWithinRadius(double lat1, double lon1,
+ double lat2, double lon2,
+ double radiusKm) {
+ double lat = Math.toRadians(lat2 - lat1);
+ double lon = Math.toRadians(lon2 - lon1);
+
+ double a = Math.sin(lat / 2) * Math.sin(lat / 2)
+ + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
+ * Math.sin(lon / 2) * Math.sin(lon / 2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ double distance = EARTH_RADIUS_KM * c;
+ return distance <= radiusKm;
+ }
+
+ private static String getLocationName(String json) {
+ JSONObject obj = new JSONObject(json);
+ String displayName = obj.getString("display_name");
+ String[] parts = displayName.split(",\\s*");
+ return Arrays.stream(parts)
+ .filter(s -> !CITY.equals(s))
+ .filter(s -> !COUNTRY.equals(s))
+ .filter(s -> !SUBURB.equals(s))
+ .collect(Collectors.joining(", "));
+ }
+
+ private static double[] getLatLon(String districtName) throws IOException {
+ String query = NOMINATIM_URL + "search?q="
+ + URLEncoder.encode(districtName + " район, "
+ + CITY, StandardCharsets.UTF_8)
+ + "&format=json&limit=1";
+
+ JSONObject obj = getFirstResultFromApi(query);
+ double lat = obj.getDouble("lat");
+ double lon = obj.getDouble("lon");
+
+ return new double[]{lat, lon};
+ }
+
+ public static String getLocalNameByLatLon(double lat, double lon) {
+ String query = NOMINATIM_URL + "reverse?lat=" + lat + "&lon=" + lon + "&format=json";
+ try {
+ JSONObject obj = getJsonObjectFromUrl(query);
+ return getLocationName(obj.toString());
+ } catch (Exception e) {
+ return "Not found";
+ }
+ }
+
+ public static boolean isEtLocal(double myLat,
+ double myLon,
+ String districtName) throws IOException {
+ double[] latLon = getLatLon(districtName);
+ return isWithinRadius(myLat, myLon, latLon[0], latLon[1], DEFAULT_RADIUS_KM);
+ }
+
+ private static JSONObject getFirstResultFromApi(String urlString) throws IOException {
+ JSONArray arr = new JSONArray(getResponseFromUrl(urlString));
+ if (arr.isEmpty()) {
+ throw new IllegalArgumentException("No location found for: " + urlString);
+ }
+ return arr.getJSONObject(0);
+ }
+
+ private static JSONObject getJsonObjectFromUrl(String urlString) throws IOException {
+ return new JSONObject(getResponseFromUrl(urlString));
+ }
+
+ private static String getResponseFromUrl(String urlString) throws IOException {
+ URL url = new URL(urlString);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestProperty("User-Agent", USER_AGENT);
+
+ if (conn.getResponseCode() != 200) {
+ throw new IOException("HTTP error: " + conn.getResponseCode());
+ }
+
+ try (InputStream in = conn.getInputStream()) {
+ return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+}
diff --git a/src/main/java/back/activitymanager/controller/ActivityController.java b/src/main/java/back/activitymanager/controller/ActivityController.java
index 7c719e1..a9743be 100644
--- a/src/main/java/back/activitymanager/controller/ActivityController.java
+++ b/src/main/java/back/activitymanager/controller/ActivityController.java
@@ -2,10 +2,12 @@
import back.activitymanager.dto.activity.ActivityCreateRequestDto;
import back.activitymanager.dto.activity.ActivityDto;
+import back.activitymanager.dto.activity.ActivitySearchDto;
import back.activitymanager.service.ActivityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -96,4 +98,12 @@ public Page getMeParticipating(Pageable pageable,
Authentication authentication) {
return activityService.getMeParticipating(pageable, authentication);
}
+
+ @Operation(summary = "Search activity")
+ @PostMapping("/search")
+ @PreAuthorize("hasRole('USER')")
+ public List searchActivity(ActivitySearchDto activitySearchDto) {
+ return activityService.searchActivity(activitySearchDto);
+ }
+
}
diff --git a/src/main/java/back/activitymanager/dto/activity/ActivityDto.java b/src/main/java/back/activitymanager/dto/activity/ActivityDto.java
index 6b228c5..918e764 100644
--- a/src/main/java/back/activitymanager/dto/activity/ActivityDto.java
+++ b/src/main/java/back/activitymanager/dto/activity/ActivityDto.java
@@ -22,9 +22,7 @@ public class ActivityDto {
private String format;
- private double lat;
-
- private double lng;
+ private String local;
private int imgId;
diff --git a/src/main/java/back/activitymanager/dto/activity/ActivitySearchDto.java b/src/main/java/back/activitymanager/dto/activity/ActivitySearchDto.java
new file mode 100644
index 0000000..0114d0a
--- /dev/null
+++ b/src/main/java/back/activitymanager/dto/activity/ActivitySearchDto.java
@@ -0,0 +1,13 @@
+package back.activitymanager.dto.activity;
+
+import java.time.LocalDateTime;
+import lombok.Data;
+
+@Data
+public class ActivitySearchDto {
+ private String local;
+ private String forWho;
+ private String category;
+ private String format;
+ private LocalDateTime dateTime;
+}
diff --git a/src/main/java/back/activitymanager/exception/GlobalExceptionHandler.java b/src/main/java/back/activitymanager/exception/GlobalExceptionHandler.java
index 661c78f..09c3e8c 100644
--- a/src/main/java/back/activitymanager/exception/GlobalExceptionHandler.java
+++ b/src/main/java/back/activitymanager/exception/GlobalExceptionHandler.java
@@ -78,4 +78,11 @@ public ResponseEntity handleNoAccessException(
return ResponseEntity.status(HttpStatus.CONFLICT)
.body("No access: " + ex.getMessage());
}
+
+ @ExceptionHandler
+ public ResponseEntity handleLocalNotFoundException(
+ LocalNotFoundException ex) {
+ return ResponseEntity.status(HttpStatus.CONFLICT)
+ .body("Local error: " + ex.getMessage());
+ }
}
diff --git a/src/main/java/back/activitymanager/exception/LocalNotFoundException.java b/src/main/java/back/activitymanager/exception/LocalNotFoundException.java
new file mode 100644
index 0000000..0ee999f
--- /dev/null
+++ b/src/main/java/back/activitymanager/exception/LocalNotFoundException.java
@@ -0,0 +1,7 @@
+package back.activitymanager.exception;
+
+public class LocalNotFoundException extends RuntimeException {
+ public LocalNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/back/activitymanager/mapper/ActivityMapper.java b/src/main/java/back/activitymanager/mapper/ActivityMapper.java
index 3a4c20b..cc44705 100644
--- a/src/main/java/back/activitymanager/mapper/ActivityMapper.java
+++ b/src/main/java/back/activitymanager/mapper/ActivityMapper.java
@@ -1,5 +1,6 @@
package back.activitymanager.mapper;
+import back.activitymanager.api.LocalAdaptorApi;
import back.activitymanager.config.MapperConfig;
import back.activitymanager.dto.activity.ActivityCreateRequestDto;
import back.activitymanager.dto.activity.ActivityDto;
@@ -13,6 +14,7 @@
public interface ActivityMapper {
@Mapping(target = "currentNumberOfPeople", ignore = true)
+ @Mapping(target = "local", ignore = true)
ActivityDto toDto(Activity activity);
@AfterMapping
@@ -21,6 +23,15 @@ default void setCurrentNumberOfPeople(
activityDto.setCurrentNumberOfPeople(activity.getParticipants().size());
}
+ @AfterMapping
+ default void setLocal(
+ @MappingTarget ActivityDto activityDto, Activity activity) {
+ activityDto.setLocal(LocalAdaptorApi
+ .getLocalNameByLatLon(
+ activity.getLat(),
+ activity.getLng()));
+ }
+
@Mapping(target = "author", ignore = true)
@Mapping(target = "participants", ignore = true)
Activity toObjFromCreateRequest(
diff --git a/src/main/java/back/activitymanager/repository/ActivityRepository.java b/src/main/java/back/activitymanager/repository/ActivityRepository.java
index 54ed4d7..0162ad8 100644
--- a/src/main/java/back/activitymanager/repository/ActivityRepository.java
+++ b/src/main/java/back/activitymanager/repository/ActivityRepository.java
@@ -7,10 +7,13 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-public interface ActivityRepository extends JpaRepository {
+public interface ActivityRepository extends JpaRepository,
+ JpaSpecificationExecutor {
+
boolean existsByAuthor(User principal);
@EntityGraph(attributePaths = {"participants", "author"})
diff --git a/src/main/java/back/activitymanager/repository/ActivitySpecification.java b/src/main/java/back/activitymanager/repository/ActivitySpecification.java
new file mode 100644
index 0000000..9cd9a74
--- /dev/null
+++ b/src/main/java/back/activitymanager/repository/ActivitySpecification.java
@@ -0,0 +1,29 @@
+package back.activitymanager.repository;
+
+import back.activitymanager.model.Activity;
+import java.time.LocalDateTime;
+import org.springframework.data.jpa.domain.Specification;
+
+public class ActivitySpecification {
+
+ public static Specification hasForWho(String forWho) {
+ return (root, query, cb) -> forWho == null ? null :
+ cb.equal(root.get("forWho"), forWho);
+ }
+
+ public static Specification hasCategory(String category) {
+ return (root, query, cb) -> category == null ? null :
+ cb.equal(root.get("category"), category);
+ }
+
+ public static Specification hasFormat(String format) {
+ return (root, query, cb) -> format == null ? null :
+ cb.equal(root.get("format"), format);
+ }
+
+ public static Specification afterDate(LocalDateTime dateTime) {
+ return (root, query, cb) -> dateTime == null ? null :
+ cb.greaterThanOrEqualTo(root.get("localDateTime"), dateTime);
+ }
+
+}
diff --git a/src/main/java/back/activitymanager/service/ActivityService.java b/src/main/java/back/activitymanager/service/ActivityService.java
index 184fdd9..a8dd6df 100644
--- a/src/main/java/back/activitymanager/service/ActivityService.java
+++ b/src/main/java/back/activitymanager/service/ActivityService.java
@@ -2,6 +2,8 @@
import back.activitymanager.dto.activity.ActivityCreateRequestDto;
import back.activitymanager.dto.activity.ActivityDto;
+import back.activitymanager.dto.activity.ActivitySearchDto;
+import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
@@ -24,4 +26,6 @@ Page getMyActivities(Pageable pageable,
Authentication authentication);
Page getMeParticipating(Pageable pageable, Authentication authentication);
+
+ List searchActivity(ActivitySearchDto activitySearchDto);
}
diff --git a/src/main/java/back/activitymanager/service/impl/ActivityServiceImpl.java b/src/main/java/back/activitymanager/service/impl/ActivityServiceImpl.java
index e27bab8..3e59ac1 100644
--- a/src/main/java/back/activitymanager/service/impl/ActivityServiceImpl.java
+++ b/src/main/java/back/activitymanager/service/impl/ActivityServiceImpl.java
@@ -1,18 +1,24 @@
package back.activitymanager.service.impl;
+import back.activitymanager.api.LocalAdaptorApi;
import back.activitymanager.dto.activity.ActivityCreateRequestDto;
import back.activitymanager.dto.activity.ActivityDto;
+import back.activitymanager.dto.activity.ActivitySearchDto;
import back.activitymanager.exception.EntityNotFoundException;
+import back.activitymanager.exception.LocalNotFoundException;
import back.activitymanager.exception.NoAccessException;
import back.activitymanager.mapper.ActivityMapper;
import back.activitymanager.model.Activity;
import back.activitymanager.model.User;
import back.activitymanager.repository.ActivityRepository;
+import back.activitymanager.repository.ActivitySpecification;
import back.activitymanager.repository.UserRepository;
import back.activitymanager.service.ActivityService;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
@@ -44,7 +50,6 @@ public Page getAll(Pageable pageable) {
@Override
public ActivityDto getById(Long id) {
-
return activityMapper.toDto(activityRepository
.findById(id)
.orElseThrow(
@@ -119,4 +124,31 @@ public Page getMeParticipating(Pageable pageable,
.map(activityMapper::toDto);
}
+ private static boolean searchActivityHelper(double lat, double lng, String local)
+ throws LocalNotFoundException {
+ try {
+ return LocalAdaptorApi.isEtLocal(lat, lng, local);
+ } catch (Exception e) {
+ throw new LocalNotFoundException("Local is not found: " + local);
+ }
+ }
+
+ @Override
+ public List searchActivity(ActivitySearchDto activitySearchDto) {
+
+ Specification spec = Specification
+ .where(ActivitySpecification.hasForWho(activitySearchDto.getForWho()))
+ .and(ActivitySpecification.hasCategory(activitySearchDto.getCategory()))
+ .and(ActivitySpecification.hasFormat(activitySearchDto.getFormat()))
+ .and(ActivitySpecification.afterDate(activitySearchDto.getDateTime()));
+
+ List all = activityRepository.findAll(spec)
+ .stream()
+ .filter(a -> searchActivityHelper(a.getLat(),
+ a.getLng(), activitySearchDto.getLocal()))
+ .toList();
+
+ return all.stream().map(activityMapper::toDto).toList();
+ }
+
}