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(); + } + }