@@ -47,7 +47,8 @@ public class HandlerConfig {
|
|||||||
StatisticSubscriberHandler statisticSubscriberHandler,
|
StatisticSubscriberHandler statisticSubscriberHandler,
|
||||||
StatisticTokenHandler statisticTokenHandler,
|
StatisticTokenHandler statisticTokenHandler,
|
||||||
StatisticIncomeHandler statisticIncomeHandler,
|
StatisticIncomeHandler statisticIncomeHandler,
|
||||||
MonitoringStatisticHandler monitoringStatisticHandler
|
MonitoringStatisticHandler monitoringStatisticHandler,
|
||||||
|
DockViewFileHandler dockViewFileHandler
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Map<Integer, RequestHandler> map = new HashMap<>();
|
Map<Integer, RequestHandler> map = new HashMap<>();
|
||||||
@@ -87,6 +88,7 @@ public class HandlerConfig {
|
|||||||
map.put(30025, statisticTokenHandler);
|
map.put(30025, statisticTokenHandler);
|
||||||
map.put(30026, statisticIncomeHandler);
|
map.put(30026, statisticIncomeHandler);
|
||||||
map.put(30027, monitoringStatisticHandler);
|
map.put(30027, monitoringStatisticHandler);
|
||||||
|
map.put(30028, dockViewFileHandler);
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,20 @@ public class DocViewerRequest {
|
|||||||
|
|
||||||
@JsonProperty("user_agent")
|
@JsonProperty("user_agent")
|
||||||
private String userAgent;
|
private String userAgent;
|
||||||
|
|
||||||
|
@JsonProperty("page")
|
||||||
|
private int page = 0;
|
||||||
|
|
||||||
|
@JsonProperty("size")
|
||||||
|
private int size = 10;
|
||||||
|
|
||||||
|
@JsonProperty("created_at")
|
||||||
|
private String sortBy = "createdAt";
|
||||||
|
|
||||||
|
@JsonProperty("sort_direction")
|
||||||
|
private String sortDirection = "desc";
|
||||||
|
|
||||||
|
@JsonProperty("action")
|
||||||
|
private String action;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ru.soune.nocopy.dto.docviewer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DockViewResponse {
|
||||||
|
|
||||||
|
@JsonProperty("city")
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@JsonProperty("country")
|
||||||
|
private String country;
|
||||||
|
|
||||||
|
@JsonProperty("viewer")
|
||||||
|
private String viewer;
|
||||||
|
|
||||||
|
@JsonProperty("document")
|
||||||
|
private String document;
|
||||||
|
|
||||||
|
@JsonProperty("view_date")
|
||||||
|
private LocalDateTime viewDate;
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ public class DockViewStatsResponse {
|
|||||||
|
|
||||||
private List<DockViewStatsResponse> content;
|
private List<DockViewStatsResponse> content;
|
||||||
|
|
||||||
|
private List<DockViewResponse> history;
|
||||||
|
|
||||||
private Integer pageNumber;
|
private Integer pageNumber;
|
||||||
|
|
||||||
private Integer pageSize;
|
private Integer pageSize;
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package ru.soune.nocopy.handler;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import ru.soune.nocopy.dto.BaseRequest;
|
||||||
|
import ru.soune.nocopy.dto.BaseResponse;
|
||||||
|
import ru.soune.nocopy.dto.MessageCode;
|
||||||
|
import ru.soune.nocopy.dto.docviewer.DocViewerRequest;
|
||||||
|
import ru.soune.nocopy.dto.docviewer.DockViewStatsResponse;
|
||||||
|
import ru.soune.nocopy.dto.file.ActionResponse;
|
||||||
|
import ru.soune.nocopy.entity.file.FileEntity;
|
||||||
|
import ru.soune.nocopy.exception.NotFoundAuthToken;
|
||||||
|
import ru.soune.nocopy.service.dockview.DockViewService;
|
||||||
|
import ru.soune.nocopy.service.file.FileEntityService;
|
||||||
|
import ru.soune.nocopy.service.register.AuthService;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class DockViewFileHandler implements RequestHandler {
|
||||||
|
|
||||||
|
private final DockViewService dockViewService;
|
||||||
|
|
||||||
|
private final FileEntityService fileEntityService;
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseResponse handle(BaseRequest request) throws Exception {
|
||||||
|
DocViewerRequest docViewerRequest = objectMapper.convertValue(request.getMessageBody(), DocViewerRequest.class);
|
||||||
|
String action = docViewerRequest.getAction();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "history_view":
|
||||||
|
return handleGetHistory(request, docViewerRequest);
|
||||||
|
case "view_stats":
|
||||||
|
return handleGetViewStats(request, docViewerRequest);
|
||||||
|
default:
|
||||||
|
ActionResponse response = ActionResponse.builder()
|
||||||
|
.action(action)
|
||||||
|
.availableActions(Arrays.asList("history_view", "view_stats"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(),
|
||||||
|
"Invalid action: " + action, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseResponse handleGetHistory(BaseRequest request, DocViewerRequest docViewerRequest) {
|
||||||
|
try {
|
||||||
|
Long userId = authService.useUserAuthToken(docViewerRequest.getToken());
|
||||||
|
List<String> fileIds = fileEntityService.getAllUserFiles(userId)
|
||||||
|
.stream()
|
||||||
|
.map(FileEntity::getId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (fileIds.isEmpty()) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.FILE_NOT_FOUND.getCode(),
|
||||||
|
"Files for search not found",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
DockViewStatsResponse openLastHistory = dockViewService.getOpenLastHistory(docViewerRequest.getPage(),
|
||||||
|
docViewerRequest.getSize(), docViewerRequest.getSortDirection(), docViewerRequest.getSortBy(),
|
||||||
|
fileIds);
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
MessageCode.SUCCESS.getDescription(),
|
||||||
|
openLastHistory);
|
||||||
|
|
||||||
|
} catch (NotFoundAuthToken e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_TOKEN.getCode(),
|
||||||
|
"Authentication required",
|
||||||
|
null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error getting view history", e);
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.FILE_NOT_FOUND.getCode(),
|
||||||
|
"Failed to get file view history: " + e.getMessage(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseResponse handleGetViewStats(BaseRequest request, DocViewerRequest docViewerRequest) {
|
||||||
|
try {
|
||||||
|
Long userId = authService.useUserAuthToken(docViewerRequest.getToken());
|
||||||
|
List<String> fileIds = fileEntityService.getAllUserFiles(userId)
|
||||||
|
.stream()
|
||||||
|
.map(FileEntity::getId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (fileIds.isEmpty()) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.FILE_NOT_FOUND.getCode(),
|
||||||
|
"Files for search not found",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DockViewStatsResponse> statsByFileIds = dockViewService.getStatsByFileIds(fileIds);
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.SUCCESS.getCode(),
|
||||||
|
MessageCode.SUCCESS.getDescription(),
|
||||||
|
statsByFileIds);
|
||||||
|
} catch (NotFoundAuthToken e) {
|
||||||
|
return new BaseResponse(request.getMsgId(),
|
||||||
|
MessageCode.INVALID_TOKEN.getCode(),
|
||||||
|
"Authentication required",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ public interface DockViewRepository extends JpaRepository<DockView, Long> {
|
|||||||
|
|
||||||
Page<DockView> findAllByFileIdOrderByCreatedAtDesc(String fileId, Pageable pageable);
|
Page<DockView> findAllByFileIdOrderByCreatedAtDesc(String fileId, Pageable pageable);
|
||||||
|
|
||||||
|
Page<DockView> findAllByFileId(Pageable pageable, List<String> fileId);
|
||||||
|
|
||||||
Page<DockView> findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable);
|
Page<DockView> findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable);
|
||||||
|
|
||||||
long countByFileId(String fileId);
|
long countByFileId(String fileId);
|
||||||
@@ -59,6 +61,18 @@ public interface DockViewRepository extends JpaRepository<DockView, Long> {
|
|||||||
"GROUP BY dv.fileId")
|
"GROUP BY dv.fileId")
|
||||||
Optional<Object[]> findViewsStatsByFileId(@Param("fileId") String fileId);
|
Optional<Object[]> findViewsStatsByFileId(@Param("fileId") String fileId);
|
||||||
|
|
||||||
|
@Query("SELECT dv.fileId, " +
|
||||||
|
"COUNT(dv) as totalViews, " +
|
||||||
|
"COUNT(DISTINCT dv.viewerIp) as uniqueIps, " +
|
||||||
|
"COUNT(DISTINCT dv.viewerId) as uniqueUsers, " +
|
||||||
|
"MAX(dv.createdAt) as lastViewDate, " +
|
||||||
|
"MIN(dv.createdAt) as firstViewDate " +
|
||||||
|
"FROM DockView dv " +
|
||||||
|
"WHERE dv.fileId IN :fileIds " +
|
||||||
|
"GROUP BY dv.fileId")
|
||||||
|
List<Object[]> findViewsStatsByFileIds(@Param("fileIds") List<String> fileIds);
|
||||||
|
|
||||||
|
|
||||||
void deleteAllByFileId(String fileId);
|
void deleteAllByFileId(String fileId);
|
||||||
|
|
||||||
void deleteAllByCreatedAtBefore(LocalDateTime date);
|
void deleteAllByCreatedAtBefore(LocalDateTime date);
|
||||||
|
|||||||
@@ -3,13 +3,21 @@ package ru.soune.nocopy.service.dockview;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import ru.soune.nocopy.dto.docviewer.DocViewerRequest;
|
import ru.soune.nocopy.dto.docviewer.DocViewerRequest;
|
||||||
|
import ru.soune.nocopy.dto.docviewer.DockViewResponse;
|
||||||
import ru.soune.nocopy.dto.docviewer.DockViewStatsResponse;
|
import ru.soune.nocopy.dto.docviewer.DockViewStatsResponse;
|
||||||
import ru.soune.nocopy.entity.docviewer.DockView;
|
import ru.soune.nocopy.entity.docviewer.DockView;
|
||||||
|
import ru.soune.nocopy.entity.file.FileEntity;
|
||||||
|
import ru.soune.nocopy.entity.user.User;
|
||||||
import ru.soune.nocopy.repository.DockViewRepository;
|
import ru.soune.nocopy.repository.DockViewRepository;
|
||||||
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||||
|
import ru.soune.nocopy.repository.UserRepository;
|
||||||
|
import ru.soune.nocopy.service.geo.GeoCountryService;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -22,6 +30,12 @@ public class DockViewService {
|
|||||||
|
|
||||||
private final DockViewRepository dockViewRepository;
|
private final DockViewRepository dockViewRepository;
|
||||||
|
|
||||||
|
private final GeoCountryService geoCountryService;
|
||||||
|
|
||||||
|
private final FileEntityRepository fileEntityRepository;
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DockView createDockView(DocViewerRequest request, Long userId) {
|
public DockView createDockView(DocViewerRequest request, Long userId) {
|
||||||
DockView dockView = new DockView();
|
DockView dockView = new DockView();
|
||||||
@@ -65,6 +79,86 @@ public class DockViewService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<DockViewStatsResponse> getStatsByFileIds(List<String> fileIds) {
|
||||||
|
List<Object[]> statsList = dockViewRepository.findViewsStatsByFileIds(fileIds);
|
||||||
|
|
||||||
|
Map<String, Object[]> statsMap = statsList.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
stats -> (String) stats[0],
|
||||||
|
stats -> stats
|
||||||
|
));
|
||||||
|
|
||||||
|
return fileIds.stream()
|
||||||
|
.map(fileId -> {
|
||||||
|
Object[] stats = statsMap.get(fileId);
|
||||||
|
|
||||||
|
if (stats == null) {
|
||||||
|
return DockViewStatsResponse.builder()
|
||||||
|
.fileId(fileId)
|
||||||
|
.totalViews(0L)
|
||||||
|
.uniqueIps(0L)
|
||||||
|
.uniqueUsers(0L)
|
||||||
|
.firstViewDate(null)
|
||||||
|
.lastViewDate(null)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return DockViewStatsResponse.builder()
|
||||||
|
.fileId((String) stats[0])
|
||||||
|
.totalViews((Long) stats[1])
|
||||||
|
.uniqueIps((Long) stats[2])
|
||||||
|
.uniqueUsers((Long) stats[3])
|
||||||
|
.lastViewDate((LocalDateTime) stats[4])
|
||||||
|
.firstViewDate((LocalDateTime) stats[5])
|
||||||
|
.build();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockViewStatsResponse getOpenLastHistory(int page, int size, String sortDirection, String sortBy,
|
||||||
|
List<String> fileIds) {
|
||||||
|
Pageable pageable = PageRequest.of(page, size,
|
||||||
|
Sort.by(Sort.Direction.fromString(sortDirection), sortBy));
|
||||||
|
|
||||||
|
Page<DockView> dockViews = dockViewRepository.findAllByFileId(pageable, fileIds);
|
||||||
|
|
||||||
|
if (dockViews.isEmpty()) {
|
||||||
|
return DockViewStatsResponse.builder()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DockViewResponse> history = new ArrayList<>();
|
||||||
|
|
||||||
|
for (DockView dockView: dockViews) {
|
||||||
|
String country = geoCountryService.getCountryByIp(dockView.getViewerIp());
|
||||||
|
String city = geoCountryService.getCityByIp(dockView.getViewerIp());
|
||||||
|
LocalDateTime createdAt = dockView.getCreatedAt();
|
||||||
|
String fileId = dockView.getFileId();
|
||||||
|
|
||||||
|
FileEntity file = fileEntityRepository.findByFileId(fileId);
|
||||||
|
Long viewerId = dockView.getViewerId();
|
||||||
|
User viewer = userRepository.findById(viewerId).orElse(null);
|
||||||
|
|
||||||
|
DockViewResponse response = DockViewResponse.builder()
|
||||||
|
.city(city)
|
||||||
|
.country(country)
|
||||||
|
.viewDate(createdAt)
|
||||||
|
.document(file.getOriginalFileName())
|
||||||
|
.viewer(viewer == null ? "guest" : viewer.getFullName())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
history.add(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DockViewStatsResponse.builder()
|
||||||
|
.history(history)
|
||||||
|
.pageNumber(dockViews.getNumber())
|
||||||
|
.pageSize(dockViews.getSize())
|
||||||
|
.totalPages(dockViews.getTotalPages())
|
||||||
|
.totalElements(dockViews.getTotalElements())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public DockViewStatsResponse getBasicStatsByFileId(String fileId) {
|
public DockViewStatsResponse getBasicStatsByFileId(String fileId) {
|
||||||
long totalViews = dockViewRepository.countByFileId(fileId);
|
long totalViews = dockViewRepository.countByFileId(fileId);
|
||||||
long uniqueIps = dockViewRepository.countUniqueIpsByFileId(fileId);
|
long uniqueIps = dockViewRepository.countUniqueIpsByFileId(fileId);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ru.soune.nocopy.service.geo;
|
|||||||
|
|
||||||
import com.maxmind.geoip2.DatabaseReader;
|
import com.maxmind.geoip2.DatabaseReader;
|
||||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
||||||
|
import com.maxmind.geoip2.model.CityResponse;
|
||||||
import com.maxmind.geoip2.model.CountryResponse;
|
import com.maxmind.geoip2.model.CountryResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
@@ -15,11 +16,17 @@ import java.net.InetAddress;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class GeoCountryService {
|
public class GeoCountryService {
|
||||||
|
|
||||||
private final DatabaseReader dbReader;
|
private final DatabaseReader countryDbReader;
|
||||||
|
|
||||||
|
private final DatabaseReader cityDbReader;
|
||||||
|
|
||||||
public GeoCountryService() throws IOException {
|
public GeoCountryService() throws IOException {
|
||||||
Resource resource = new ClassPathResource("geo/GeoLite2-Country.mmdb");
|
Resource resource = new ClassPathResource("geo/GeoLite2-Country.mmdb");
|
||||||
this.dbReader = new DatabaseReader.Builder(resource.getInputStream()).build();
|
this.countryDbReader = new DatabaseReader.Builder(resource.getInputStream()).build();
|
||||||
|
|
||||||
|
Resource cityResource = new ClassPathResource("geo/GeoLite2-City.mmdb");
|
||||||
|
this.cityDbReader = new DatabaseReader.Builder(cityResource.getInputStream()).build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCountryName(String input) {
|
public String getCountryName(String input) {
|
||||||
@@ -44,12 +51,48 @@ public class GeoCountryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCityByIp(String ip) {
|
||||||
|
try {
|
||||||
|
InetAddress ipAddress = InetAddress.getByName(ip);
|
||||||
|
CityResponse response = cityDbReader.city(ipAddress);
|
||||||
|
|
||||||
|
return response.getCity().getName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get city for IP: {}", ip, e);
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCountryByIp(String ip) {
|
||||||
|
try {
|
||||||
|
InetAddress ipAddress = InetAddress.getByName(ip);
|
||||||
|
CountryResponse response = countryDbReader.country(ipAddress);
|
||||||
|
|
||||||
|
return response.getCountry().getName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get country for IP: {}", ip, e);
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCountryCodeByIp(String ip) {
|
||||||
|
try {
|
||||||
|
InetAddress ipAddress = InetAddress.getByName(ip);
|
||||||
|
CountryResponse response = countryDbReader.country(ipAddress);
|
||||||
|
|
||||||
|
return response.getCountry().getIsoCode();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get country code for IP: {}", ip, e);
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CountryResponse extractCountry(String input) throws IOException, GeoIp2Exception {
|
private CountryResponse extractCountry(String input) throws IOException, GeoIp2Exception {
|
||||||
String domain = extractDomain(input);
|
String domain = extractDomain(input);
|
||||||
|
|
||||||
InetAddress ip = InetAddress.getByName(domain);
|
InetAddress ip = InetAddress.getByName(domain);
|
||||||
|
|
||||||
return dbReader.country(ip);
|
return countryDbReader.country(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractDomain(String input) {
|
private String extractDomain(String input) {
|
||||||
|
|||||||
Reference in New Issue
Block a user