initial commit

This commit is contained in:
2021-10-03 17:07:01 +02:00
commit 456332f24e
246 changed files with 24590 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-main</artifactId>
<version>${revision}</version>
</parent>
<name>urlshortener</name>
<artifactId>webstly-urlshortener</artifactId>
<dependencies>
<dependency>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-core</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,316 @@
/**
*
*/
package de.bstly.we.urlshortener.businesslogic;
import java.time.Instant;
import java.util.List;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.google.common.collect.Lists;
import com.querydsl.core.BooleanBuilder;
import de.bstly.we.businesslogic.QuotaManager;
import de.bstly.we.businesslogic.SystemPropertyManager;
import de.bstly.we.businesslogic.UserDataProvider;
import de.bstly.we.model.Quota;
import de.bstly.we.model.UserData;
import de.bstly.we.urlshortener.model.QShortenedUrl;
import de.bstly.we.urlshortener.model.ShortenedUrl;
import de.bstly.we.urlshortener.repository.ShortenedUrlRepository;
/**
* @author _bastler@bstly.de
*
*/
@Component
public class ShortenedUrlManager implements SmartInitializingSingleton, UserDataProvider {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private Environment environment;
@Autowired
private SystemPropertyManager systemPropertyManager;
@Autowired
private QuotaManager quotaManager;
@Autowired
private ShortenedUrlRepository shortenedUrlRepository;
private QShortenedUrl qShortenedUrl = QShortenedUrl.shortenedUrl;
private int codeLength;
public static final String SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH = "urlShortener.codeLength";
public static final String SYSTEM_PROPERTY_URL_SHORTENER_LINK = "urlShortener.link";
public static final String SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND = "urlShortener.notFoundRedirect";
public static final String SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD = "urlShortener.passwordRedirect";
/*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
@Override
public void afterSingletonsInstantiated() {
if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH)) {
systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH,
environment.getProperty("we.bstly.urlShortener.codeLength", "6"));
}
if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_LINK)) {
systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_LINK,
environment.getProperty("we.bstly.urlShortener.link", ""));
}
if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND)) {
systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND,
environment.getProperty("we.bstly.urlShortener.notFoundRedirect", ""));
}
if (!systemPropertyManager.has(SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD)) {
systemPropertyManager.add(SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD,
environment.getProperty("we.bstly.urlShortener.passwordRedirect", ""));
}
codeLength = systemPropertyManager.getInteger(SYSTEM_PROPERTY_URL_SHORTENER_CODE_LENGTH);
}
/**
*
* @param code
* @return
*/
public ShortenedUrl get(String code) {
return shortenedUrlRepository.findById(code).orElse(null);
}
/**
*
* @param userId
* @return
*/
public List<ShortenedUrl> getAllByOwner(Long userId) {
return Lists.newArrayList(shortenedUrlRepository.findAll(qShortenedUrl.owner.eq(userId)));
}
/**
*
* @param page
* @param size
* @param sortBy
* @param descending
* @param search
* @return
*/
public Page<ShortenedUrl> get(int page, int size, String sortBy, boolean descending,
String search) {
if (StringUtils.hasText(search)) {
return shortenedUrlRepository.findAll(
qShortenedUrl.note.containsIgnoreCase(search)
.or(qShortenedUrl.url.containsIgnoreCase(search))
.or(qShortenedUrl.link.containsIgnoreCase(search)),
PageRequest.of(page, size, descending ? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending()));
}
return shortenedUrlRepository.findAll(PageRequest.of(page, size,
descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()));
}
/**
*
* @param userId
* @param page
* @param size
* @param sortBy
* @param descending
* @param search
* @return
*/
public Page<ShortenedUrl> getForUserId(Long userId, int page, int size, String sortBy,
boolean descending, String search) {
// delete expired urls
shortenedUrlRepository.deleteAll(shortenedUrlRepository.findAll(
qShortenedUrl.owner.eq(userId).and(qShortenedUrl.expires.before(Instant.now()))));
BooleanBuilder query = new BooleanBuilder();
query.and(qShortenedUrl.owner.eq(userId));
if (StringUtils.hasText(search)) {
query.and(qShortenedUrl.note.containsIgnoreCase(search)
.or(qShortenedUrl.url.containsIgnoreCase(search))
.or(qShortenedUrl.link.containsIgnoreCase(search)));
}
return shortenedUrlRepository.findAll(query.getValue(), PageRequest.of(page, size,
descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending()));
}
/**
*
* @param owner
* @param url
* @param code
* @param expires
* @param password
* @param quota
* @return
*/
public ShortenedUrl create(Long owner, String url, String note, String code, Instant expires,
String password, boolean queryParameters, boolean quota) {
ShortenedUrl shortenedUrl = new ShortenedUrl();
shortenedUrl.setOwner(owner);
shortenedUrl.setUrl(url);
shortenedUrl.setNote(note);
if (StringUtils.hasText(code)) {
Assert.isTrue(!shortenedUrlRepository.existsById(code), "Given code already exists!");
} else {
code = RandomStringUtils.random(codeLength, true, true).toUpperCase();
while (shortenedUrlRepository.existsById(code)) {
code = RandomStringUtils.random(codeLength, true, true).toUpperCase();
}
}
shortenedUrl.setCode(code);
shortenedUrl.setExpires(expires);
shortenedUrl.setQueryParameters(queryParameters);
if (StringUtils.hasText(password)) {
shortenedUrl.setPasswordHash(passwordEncoder.encode(password));
}
String link = systemPropertyManager.get(SYSTEM_PROPERTY_URL_SHORTENER_LINK);
if (StringUtils.hasText(link)) {
shortenedUrl.setLink(String.format(link, shortenedUrl.getCode()));
}
shortenedUrl = shortenedUrlRepository.save(shortenedUrl);
if (quota) {
Quota shortenedUrlsQuota = quotaManager.get(shortenedUrl.getOwner(),
ShortenedUrlQuotas.URL_SHORTENER);
if (shortenedUrlsQuota != null) {
shortenedUrlsQuota.setValue(shortenedUrlsQuota.getValue() - 1);
quotaManager.update(shortenedUrlsQuota);
}
}
return shortenedUrl;
}
/**
*
* @param shortenedUrl
* @return
*/
public ShortenedUrl save(ShortenedUrl shortenedUrl) {
String link = systemPropertyManager.get(SYSTEM_PROPERTY_URL_SHORTENER_LINK);
if (StringUtils.hasText(link)) {
shortenedUrl.setLink(String.format(link, shortenedUrl.getCode()));
}
return shortenedUrlRepository.save(shortenedUrl);
}
/**
*
* @param shortenedUrl
* @param quota
*/
public void delete(ShortenedUrl shortenedUrl, boolean quota) {
if (quota) {
Quota shortenedUrlsQuota = quotaManager.get(shortenedUrl.getOwner(),
ShortenedUrlQuotas.URL_SHORTENER);
if (shortenedUrlsQuota == null) {
shortenedUrlsQuota = quotaManager.create(shortenedUrl.getOwner(),
ShortenedUrlQuotas.URL_SHORTENER, 0, "#", true);
}
shortenedUrlsQuota.setValue(shortenedUrlsQuota.getValue() + 1);
quotaManager.update(shortenedUrlsQuota);
}
shortenedUrlRepository.delete(shortenedUrl);
}
/**
*
* @param owner
* @param quota
*/
public void deleteAll(Long owner, boolean quota) {
List<ShortenedUrl> shortenedUrls = Lists
.newArrayList(shortenedUrlRepository.findAll(qShortenedUrl.owner.eq(owner)));
for (ShortenedUrl shortenedUrl : shortenedUrls) {
delete(shortenedUrl, quota);
}
}
/*
* @see de.bstly.we.businesslogic.UserDataProvider#getId()
*/
@Override
public String getId() {
return "shortend-urls";
}
/*
* @see de.bstly.we.businesslogic.UserDataProvider#getUserData(java.lang.Long)
*/
@Override
public List<UserData> getUserData(Long userId) {
List<UserData> result = Lists.newArrayList();
for (ShortenedUrl shortenedUrl : getAllByOwner(userId)) {
result.add(shortenedUrl);
}
return result;
}
/*
* @see de.bstly.we.businesslogic.UserDataProvider#purgeUserData(java.lang.Long)
*/
@Override
public void purgeUserData(Long userId) {
for (ShortenedUrl shortenedUrl : getAllByOwner(userId)) {
shortenedUrlRepository.delete(shortenedUrl);
}
}
/**
*
*/
@Scheduled(cron = "0 */5 * * * *")
public void clearOrUpdateShortenedUrls() {
shortenedUrlRepository.deleteAll(
shortenedUrlRepository.findAll(qShortenedUrl.expires.before(Instant.now())));
String link = systemPropertyManager.get(SYSTEM_PROPERTY_URL_SHORTENER_LINK);
String linkRegex = "";
if (StringUtils.hasText(link)) {
linkRegex = String.format(link, "%");
}
if (StringUtils.hasText(linkRegex)) {
for (ShortenedUrl shortenedUrl : shortenedUrlRepository
.findAll(qShortenedUrl.link.isNull().or(qShortenedUrl.link.isEmpty()
.or(qShortenedUrl.link.matches(linkRegex).not())))) {
shortenedUrl.setLink(String.format(link, shortenedUrl.getCode()));
shortenedUrl = shortenedUrlRepository.save(shortenedUrl);
}
}
}
}
@@ -0,0 +1,12 @@
/**
*
*/
package de.bstly.we.urlshortener.businesslogic;
/**
* @author _bastler@bstly.de
*
*/
public interface ShortenedUrlPermissions {
public static final String URL_SHORTENER = "url_shortener";
}
@@ -0,0 +1,13 @@
/**
*
*/
package de.bstly.we.urlshortener.businesslogic;
/**
* @author _bastler@bstly.de
*
*/
public interface ShortenedUrlQuotas {
public static final String URL_SHORTENER = "url_shortener";
}
@@ -0,0 +1,355 @@
/**
*
*/
package de.bstly.we.urlshortener.controller;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.we.businesslogic.PermissionManager;
import de.bstly.we.businesslogic.QuotaManager;
import de.bstly.we.businesslogic.SystemPropertyManager;
import de.bstly.we.controller.BaseController;
import de.bstly.we.controller.support.EntityResponseStatusException;
import de.bstly.we.controller.support.RequestBodyErrors;
import de.bstly.we.model.Quota;
import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager;
import de.bstly.we.urlshortener.businesslogic.ShortenedUrlPermissions;
import de.bstly.we.urlshortener.businesslogic.ShortenedUrlQuotas;
import de.bstly.we.urlshortener.controller.model.ShortenedUrlModel;
import de.bstly.we.urlshortener.controller.validation.ShortenedUrlModelValidator;
import de.bstly.we.urlshortener.model.ShortenedUrl;
/**
* @author _bastler@bstly.de
*
*/
@RestController
@RequestMapping("/url/shortener")
public class ShortenedUrlController extends BaseController {
@Autowired
private ShortenedUrlManager shortenedUrlManager;
@Autowired
private PermissionManager permissionManager;
@Autowired
private QuotaManager quotaManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SystemPropertyManager systemPropertyManager;
@Autowired
private ShortenedUrlModelValidator shortenedUrlModelValidator;
/**
*
* @param code
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/{code}")
public void getShortenedUrlLink(@PathVariable("code") String code, HttpServletRequest request,
HttpServletResponse response) throws IOException {
ShortenedUrl shortenedUrl = shortenedUrlManager.get(code);
try {
if (shortenedUrl == null) {
if (StringUtils.hasText(systemPropertyManager
.get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND))) {
response.sendRedirect(systemPropertyManager
.get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND));
} else {
response.sendError(404, "code not found");
}
} else if (StringUtils.hasText(shortenedUrl.getPasswordHash())) {
if (StringUtils.hasText(systemPropertyManager
.get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD))) {
response.sendRedirect(String.format(
systemPropertyManager.get(
ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD),
shortenedUrl.getCode()));
} else {
response.sendError(401, "password protected");
}
} else {
sendValidRedirect(shortenedUrl, request, response);
}
} catch (IOException e) {
response.sendError(400, code);
}
}
/**
*
* @param code
* @param password
* @param request
* @param response
* @throws IOException
*/
@PostMapping("/{code}")
public void getProtectedShortenedUrlLink(@PathVariable("code") String code,
@ModelAttribute("password") String password, HttpServletRequest request,
HttpServletResponse response) throws IOException {
ShortenedUrl shortenedUrl = shortenedUrlManager.get(code);
try {
if (shortenedUrl == null) {
if (StringUtils.hasText(systemPropertyManager
.get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND))) {
response.sendRedirect(systemPropertyManager
.get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_NOT_FOUND));
} else {
response.sendError(404, "code not found");
}
} else if (!StringUtils.hasText(shortenedUrl.getPasswordHash())
|| passwordEncoder.matches(password, shortenedUrl.getPasswordHash())) {
sendValidRedirect(shortenedUrl, request, response);
} else {
if (StringUtils.hasText(systemPropertyManager
.get(ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD))) {
String passwordErrorUrl = String.format(
systemPropertyManager.get(
ShortenedUrlManager.SYSTEM_PROPERTY_URL_SHORTENER_PASSWORD),
shortenedUrl.getCode());
URI passwordErrorUri;
try {
passwordErrorUri = new URI(passwordErrorUrl);
if (StringUtils.hasText(passwordErrorUri.getQuery())) {
passwordErrorUrl += "&error";
} else {
passwordErrorUrl += "?error";
}
} catch (URISyntaxException e) {
}
response.sendRedirect(passwordErrorUrl);
} else {
response.sendError(401, "password protected");
}
}
} catch (IOException e) {
response.sendError(400, code);
}
}
/**
*
* @param shortenedUrl
* @param request
* @param response
* @throws IOException
*/
protected void sendValidRedirect(ShortenedUrl shortenedUrl, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String url = shortenedUrl.getUrl();
if (shortenedUrl.isQueryParameters() && StringUtils.hasText(request.getQueryString())) {
try {
URI uri = new URI(url);
if (StringUtils.hasText(uri.getQuery())) {
url += "&" + request.getQueryString();
} else {
url += "?" + request.getQueryString();
}
} catch (URISyntaxException e) {
}
}
response.sendRedirect(url);
}
/**
*
* @param code
* @return
*/
@PreAuthorize("isAuthenticated()")
@GetMapping("/model/{code}")
public ShortenedUrl getShortenedUrl(@PathVariable("code") String code) {
ShortenedUrl shortenedUrl = shortenedUrlManager.get(code);
if (shortenedUrl == null) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
if (StringUtils.hasText(shortenedUrl.getPasswordHash())) {
shortenedUrl.setUrl(null);
shortenedUrl.setPasswordHash(null);
}
if (!shortenedUrl.getOwner().equals(getCurrentUserId())) {
shortenedUrl.setOwner(null);
shortenedUrl.setNote(null);
}
return shortenedUrl;
}
/**
*
* @return
*/
@PreAuthorize("isAuthenticated()")
@GetMapping
public Page<ShortenedUrl> getShortenedUrls(
@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter,
@RequestParam("sort") Optional<String> sortParameter,
@RequestParam("desc") Optional<Boolean> descParameter,
@RequestParam("search") Optional<String> searchParameter) {
if (!permissionManager.hasPermission(getCurrentUserId(),
ShortenedUrlPermissions.URL_SHORTENER)) {
shortenedUrlManager.deleteAll(getCurrentUserId(), true);
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
return shortenedUrlManager.getForUserId(getCurrentUserId(), pageParameter.orElse(0),
sizeParameter.orElse(10), sortParameter.orElse("code"), descParameter.orElse(false),
searchParameter.orElse(""));
}
/**
*
* @param shortenedUrlModel
* @return
*/
@PreAuthorize("isAuthenticated()")
@PostMapping
public ShortenedUrl createShortenedUrl(@RequestBody ShortenedUrlModel shortenedUrlModel) {
if (!permissionManager.hasPermission(getCurrentUserId(),
ShortenedUrlPermissions.URL_SHORTENER)
|| !permissionManager.isFullUser(getCurrentUserId())) {
shortenedUrlManager.deleteAll(getCurrentUserId(), true);
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
Quota shortenedUrlsQuota = quotaManager.get(getCurrentUserId(),
ShortenedUrlQuotas.URL_SHORTENER);
if (shortenedUrlsQuota == null || shortenedUrlsQuota.getValue() < 1) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
Errors errors = new RequestBodyErrors(shortenedUrlModel);
shortenedUrlModelValidator.validate(shortenedUrlModel, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
return shortenedUrlManager.create(getCurrentUserId(), shortenedUrlModel.getUrl(),
shortenedUrlModel.getNote(), shortenedUrlModel.getCode(),
shortenedUrlModel.getExpires(), shortenedUrlModel.getPassword(),
shortenedUrlModel.isQueryParameters(), true);
}
/**
*
* @param shortenedUrlModel
* @return
*/
@PreAuthorize("isAuthenticated()")
@PatchMapping
public ShortenedUrl updateShortenedUrl(@RequestBody ShortenedUrlModel shortenedUrlModel) {
if (!permissionManager.hasPermission(getCurrentUserId(),
ShortenedUrlPermissions.URL_SHORTENER)
|| !permissionManager.isFullUser(getCurrentUserId())) {
shortenedUrlManager.deleteAll(getCurrentUserId(), true);
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
String code = shortenedUrlModel.getCode();
boolean newShortenedUrl = false;
ShortenedUrl shortenedUrl = shortenedUrlManager.get(code);
if (shortenedUrl == null || !shortenedUrl.getOwner().equals(getCurrentUserId())) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
Errors errors = new RequestBodyErrors(shortenedUrlModel);
if (StringUtils.hasText(shortenedUrlModel.getNewCode())
&& !shortenedUrlModel.getNewCode().equals(code)) {
shortenedUrlModel.setCode(shortenedUrlModel.getNewCode());
newShortenedUrl = true;
} else {
shortenedUrlModel.setCode(null);
}
shortenedUrlModelValidator.validate(shortenedUrlModel, errors);
if (errors.hasErrors()) {
throw new EntityResponseStatusException(errors.getAllErrors(), HttpStatus.CONFLICT);
}
if (newShortenedUrl) {
String passwordHash = shortenedUrl.getPasswordHash();
shortenedUrlManager.delete(shortenedUrl, false);
shortenedUrl = new ShortenedUrl();
shortenedUrl.setCode(shortenedUrlModel.getCode());
shortenedUrl.setPasswordHash(passwordHash);
shortenedUrl.setOwner(getCurrentUserId());
}
shortenedUrl.setQueryParameters(shortenedUrlModel.isQueryParameters());
shortenedUrl.setExpires(shortenedUrlModel.getExpires());
shortenedUrl.setUrl(shortenedUrlModel.getUrl());
shortenedUrl.setNote(shortenedUrlModel.getNote());
if (shortenedUrlModel.isNewPassword()) {
if (StringUtils.hasText(shortenedUrlModel.getPassword())) {
shortenedUrl
.setPasswordHash(passwordEncoder.encode(shortenedUrlModel.getPassword()));
} else {
shortenedUrl.setPasswordHash(null);
}
}
return shortenedUrlManager.save(shortenedUrl);
}
/**
*
* @param id
*/
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/{code}")
public void deleteShortenedUrl(@PathVariable("code") String code) {
if (!permissionManager.hasPermission(getCurrentUserId(),
ShortenedUrlPermissions.URL_SHORTENER)) {
shortenedUrlManager.deleteAll(getCurrentUserId(), true);
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
ShortenedUrl shortenedUrl = shortenedUrlManager.get(code);
if (shortenedUrl == null || !shortenedUrl.getOwner().equals(getCurrentUserId())) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
shortenedUrlManager.delete(shortenedUrl, true);
}
}
@@ -0,0 +1,90 @@
/**
*
*/
package de.bstly.we.urlshortener.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.we.controller.BaseController;
import de.bstly.we.controller.support.EntityResponseStatusException;
import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager;
import de.bstly.we.urlshortener.model.ShortenedUrl;
/**
* @author _bastler@bstly.de
*
*/
@RestController
@RequestMapping("/url/shortener/manage")
public class ShortenedUrlManagementController extends BaseController {
@Autowired
private ShortenedUrlManager shortenedUrlManager;
/**
*
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping
public Page<ShortenedUrl> getShortenedUrls(
@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter,
@RequestParam("search") Optional<String> searchParameter) {
return shortenedUrlManager.get(pageParameter.orElse(0), sizeParameter.orElse(10), "code",
true, searchParameter.orElse(""));
}
/**
*
* @param shortenedUrl
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping
public ShortenedUrl createOrUpdateShortenedUrl(@RequestBody ShortenedUrl shortenedUrl) {
return shortenedUrlManager.save(shortenedUrl);
}
/**
*
* @param id
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{code}")
public void deleteShortenedUrl(@PathVariable("code") String code,
@RequestParam("quota") Optional<Boolean> quota) {
ShortenedUrl shortenedUrl = shortenedUrlManager.get(code);
if (shortenedUrl == null) {
throw new EntityResponseStatusException(HttpStatus.CONFLICT);
}
shortenedUrlManager.delete(shortenedUrl, quota.isPresent() && quota.get().booleanValue());
}
/**
*
* @param owner
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/all/{owner}")
public void deleteAll(@PathVariable("owner") Long owner,
@RequestParam("quota") Optional<Boolean> quota) {
shortenedUrlManager.deleteAll(owner, quota.isPresent() && quota.get().booleanValue());
}
}
@@ -0,0 +1,150 @@
/**
*
*/
package de.bstly.we.urlshortener.controller.model;
import java.time.Instant;
/**
* @author _bastler@bstly.de
*
*/
public class ShortenedUrlModel {
private String code;
private String newCode;
private String url;
private String note;
private Instant expires;
private String password;
private String password2;
private boolean queryParameters;
private boolean newPassword;
/**
* @return the code
*/
public String getCode() {
return code;
}
/**
* @param code the code to set
*/
public void setCode(String code) {
this.code = code;
}
/**
* @return the newCode
*/
public String getNewCode() {
return newCode;
}
/**
* @param newCode the newCode to set
*/
public void setNewCode(String newCode) {
this.newCode = newCode;
}
/**
* @return the url
*/
public String getUrl() {
return url;
}
/**
* @param url the url to set
*/
public void setUrl(String url) {
this.url = url;
}
/**
* @return the note
*/
public String getNote() {
return note;
}
/**
* @param note the note to set
*/
public void setNote(String note) {
this.note = note;
}
/**
* @return the expires
*/
public Instant getExpires() {
return expires;
}
/**
* @param expires the expires to set
*/
public void setExpires(Instant expires) {
this.expires = expires;
}
/**
* @return the password
*/
public String getPassword() {
return password;
}
/**
* @param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* @return the password2
*/
public String getPassword2() {
return password2;
}
/**
* @param password2 the password2 to set
*/
public void setPassword2(String password2) {
this.password2 = password2;
}
/**
* @return the queryParameters
*/
public boolean isQueryParameters() {
return queryParameters;
}
/**
* @param queryParameters the queryParameters to set
*/
public void setQueryParameters(boolean queryParameters) {
this.queryParameters = queryParameters;
}
/**
* @return the newPassword
*/
public boolean isNewPassword() {
return newPassword;
}
/**
* @param newPassword the newPassword to set
*/
public void setNewPassword(boolean newPassword) {
this.newPassword = newPassword;
}
}
@@ -0,0 +1,69 @@
/**
*
*/
package de.bstly.we.urlshortener.controller.validation;
import java.time.Instant;
import org.apache.commons.validator.routines.UrlValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import de.bstly.we.urlshortener.businesslogic.ShortenedUrlManager;
import de.bstly.we.urlshortener.controller.model.ShortenedUrlModel;
/**
* @author _bastler@bstly.de
*
*/
@Component
public class ShortenedUrlModelValidator implements Validator {
@Autowired
private ShortenedUrlManager shortenedUrlManager;
private UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES);
protected static final String codePart = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+(?:\\\\.[A-Z0-9_!#$%&'*+/=?`{|}~^-]+)*$";
/*
* @see org.springframework.validation.Validator#supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(ShortenedUrlModel.class);
}
/*
* @see org.springframework.validation.Validator#validate(java.lang.Object,
* org.springframework.validation.Errors)
*/
@Override
public void validate(Object target, Errors errors) {
ShortenedUrlModel shortenedUrlModel = (ShortenedUrlModel) target;
if (!urlValidator.isValid(shortenedUrlModel.getUrl())) {
errors.rejectValue("url", "INVALID_URL");
}
if (StringUtils.hasText(shortenedUrlModel.getCode())) {
if (shortenedUrlManager.get(shortenedUrlModel.getCode()) != null) {
errors.rejectValue("code", "NOT_VALID");
} else if (!shortenedUrlModel.getCode().matches(codePart)) {
errors.rejectValue("code", "NOT_VALID");
}
}
if (shortenedUrlModel.getExpires() != null
&& shortenedUrlModel.getExpires().isBefore(Instant.now())) {
errors.rejectValue("expires", "NOT_VALID");
}
if (StringUtils.hasText(shortenedUrlModel.getPassword())
&& !(shortenedUrlModel.getPassword().equals(shortenedUrlModel.getPassword2()))) {
errors.rejectValue("password", "NOT_MATCH");
}
}
}
@@ -0,0 +1,172 @@
/**
*
*/
package de.bstly.we.urlshortener.model;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.bstly.we.businesslogic.support.AbstractModelEventListener;
import de.bstly.we.model.AbstractModel;
import de.bstly.we.model.UserData;
/**
* @author _bastler@bstly.de
*
*/
@Entity
@Table(name = "shortened_urls")
@EntityListeners(AbstractModelEventListener.class)
public class ShortenedUrl implements UserData, AbstractModel {
@Id
@Column(name = "code")
private String code;
private Long owner;
@Lob
private String url;
private Instant expires;
@JsonIgnore
@Column(name = "password", nullable = true)
private String passwordHash;
private String link;
@Lob
private String note;
@Column(name = "addon", columnDefinition = "boolean default false")
private boolean queryParameters;
@JsonProperty("password")
@Transient
private boolean hasPassword;
/**
* @return the code
*/
public String getCode() {
return code;
}
/**
* @param code the code to set
*/
public void setCode(String code) {
this.code = code;
}
/**
* @return the owner
*/
public Long getOwner() {
return owner;
}
/**
* @param owner the owner to set
*/
public void setOwner(Long owner) {
this.owner = owner;
}
/**
* @return the url
*/
public String getUrl() {
return url;
}
/**
* @param url the url to set
*/
public void setUrl(String url) {
this.url = url;
}
/**
* @return the expires
*/
public Instant getExpires() {
return expires;
}
/**
* @param expires the expires to set
*/
public void setExpires(Instant expires) {
this.expires = expires;
}
/**
* @return the passwordHash
*/
public String getPasswordHash() {
return passwordHash;
}
/**
* @param passwordHash the passwordHash to set
*/
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
/**
* @return the link
*/
public String getLink() {
return link;
}
/**
* @param link the link to set
*/
public void setLink(String link) {
this.link = link;
}
/**
* @return the note
*/
public String getNote() {
return note;
}
/**
* @param note the note to set
*/
public void setNote(String note) {
this.note = note;
}
/**
* @return the queryParameters
*/
public boolean isQueryParameters() {
return queryParameters;
}
/**
* @param queryParameters the queryParameters to set
*/
public void setQueryParameters(boolean queryParameters) {
this.queryParameters = queryParameters;
}
/**
* @return the hasPassword
*/
public boolean isHasPassword() {
return StringUtils.hasText(passwordHash);
}
}
@@ -0,0 +1,20 @@
/**
*
*/
package de.bstly.we.urlshortener.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.bstly.we.urlshortener.model.ShortenedUrl;
/**
*
* @author _bastler@bstly.de
*
*/
@Repository
public interface ShortenedUrlRepository
extends JpaRepository<ShortenedUrl, String>, QuerydslPredicateExecutor<ShortenedUrl> {
}