Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@
<version>7.0.0</version>
</dependency>

<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>

</dependencies>

<build>
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/back/activitymanager/api/LocalAdaptorApi.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
104 changes: 104 additions & 0 deletions src/main/java/back/activitymanager/api/LocationApi.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,4 +98,12 @@ public Page<ActivityDto> getMeParticipating(Pageable pageable,
Authentication authentication) {
return activityService.getMeParticipating(pageable, authentication);
}

@Operation(summary = "Search activity")
@PostMapping("/search")
@PreAuthorize("hasRole('USER')")
public List<ActivityDto> searchActivity(ActivitySearchDto activitySearchDto) {
return activityService.searchActivity(activitySearchDto);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ public class ActivityDto {

private String format;

private double lat;

private double lng;
private String local;

private int imgId;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,11 @@ public ResponseEntity<String> handleNoAccessException(
return ResponseEntity.status(HttpStatus.CONFLICT)
.body("No access: " + ex.getMessage());
}

@ExceptionHandler
public ResponseEntity<String> handleLocalNotFoundException(
LocalNotFoundException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body("Local error: " + ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package back.activitymanager.exception;

public class LocalNotFoundException extends RuntimeException {
public LocalNotFoundException(String message) {
super(message);
}
}
11 changes: 11 additions & 0 deletions src/main/java/back/activitymanager/mapper/ActivityMapper.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +14,7 @@
public interface ActivityMapper {

@Mapping(target = "currentNumberOfPeople", ignore = true)
@Mapping(target = "local", ignore = true)
ActivityDto toDto(Activity activity);

@AfterMapping
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Activity, Long> {
public interface ActivityRepository extends JpaRepository<Activity, Long>,
JpaSpecificationExecutor<Activity> {

boolean existsByAuthor(User principal);

@EntityGraph(attributePaths = {"participants", "author"})
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Activity> hasForWho(String forWho) {
return (root, query, cb) -> forWho == null ? null :
cb.equal(root.get("forWho"), forWho);
}

public static Specification<Activity> hasCategory(String category) {
return (root, query, cb) -> category == null ? null :
cb.equal(root.get("category"), category);
}

public static Specification<Activity> hasFormat(String format) {
return (root, query, cb) -> format == null ? null :
cb.equal(root.get("format"), format);
}

public static Specification<Activity> afterDate(LocalDateTime dateTime) {
return (root, query, cb) -> dateTime == null ? null :
cb.greaterThanOrEqualTo(root.get("localDateTime"), dateTime);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,4 +26,6 @@ Page<ActivityDto> getMyActivities(Pageable pageable,
Authentication authentication);

Page<ActivityDto> getMeParticipating(Pageable pageable, Authentication authentication);

List<ActivityDto> searchActivity(ActivitySearchDto activitySearchDto);
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -44,7 +50,6 @@ public Page<ActivityDto> getAll(Pageable pageable) {

@Override
public ActivityDto getById(Long id) {

return activityMapper.toDto(activityRepository
.findById(id)
.orElseThrow(
Expand Down Expand Up @@ -119,4 +124,31 @@ public Page<ActivityDto> 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<ActivityDto> searchActivity(ActivitySearchDto activitySearchDto) {

Specification<Activity> spec = Specification
.where(ActivitySpecification.hasForWho(activitySearchDto.getForWho()))
.and(ActivitySpecification.hasCategory(activitySearchDto.getCategory()))
.and(ActivitySpecification.hasFormat(activitySearchDto.getFormat()))
.and(ActivitySpecification.afterDate(activitySearchDto.getDateTime()));

List<Activity> all = activityRepository.findAll(spec)
.stream()
.filter(a -> searchActivityHelper(a.getLat(),
a.getLng(), activitySearchDto.getLocal()))
.toList();

return all.stream().map(activityMapper::toDto).toList();
}

}