Refactor some code in Java8 and transform properties file to yaml file.
This commit is contained in:
25
pom.xml
25
pom.xml
@@ -14,7 +14,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>2.0.0.RELEASE</version>
|
<version>2.1.6.RELEASE</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -25,35 +25,40 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- ************* -->
|
||||||
|
<!-- Spring boot -->
|
||||||
|
<!-- ************* -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Runtime -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-annotations</artifactId>
|
<!-- <dependency>-->
|
||||||
</dependency>
|
<!-- <groupId>com.fasterxml.jackson.core</groupId>-->
|
||||||
<!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt -->
|
<!-- <artifactId>jackson-annotations</artifactId>-->
|
||||||
|
<!-- </dependency>-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mindrot</groupId>
|
<groupId>org.mindrot</groupId>
|
||||||
<artifactId>jbcrypt</artifactId>
|
<artifactId>jbcrypt</artifactId>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import org.springframework.context.annotation.ComponentScan;
|
|||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableAutoConfiguration
|
@EnableAutoConfiguration
|
||||||
@ComponentScan(basePackages = "org.codiki")
|
|
||||||
public class CodikiApplication {
|
public class CodikiApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -29,11 +29,21 @@ import com.fasterxml.jackson.annotation.JsonView;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/account")
|
@RequestMapping("/api/account")
|
||||||
public class AccountController {
|
public class AccountController {
|
||||||
@Autowired
|
/** Account service. */
|
||||||
private AccountService accountService;
|
private AccountService accountService;
|
||||||
@Autowired
|
/** User service. */
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param accountService Account service.
|
||||||
|
* @param userService User service.
|
||||||
|
*/
|
||||||
|
public AccountController(AccountService accountService, UserService userService) {
|
||||||
|
this.accountService = accountService;
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonView(View.UserDTO.class)
|
@JsonView(View.UserDTO.class)
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public User login(@RequestBody final User pUser) throws BadCredentialsException {
|
public User login(@RequestBody final User pUser) throws BadCredentialsException {
|
||||||
@@ -54,8 +64,6 @@ public class AccountController {
|
|||||||
* @param pPasswordWrapper
|
* @param pPasswordWrapper
|
||||||
* The object which contains the old password for verification and
|
* The object which contains the old password for verification and
|
||||||
* the new password to set to the user.
|
* the new password to set to the user.
|
||||||
* @param pRequest
|
|
||||||
* The request injected by Spring.
|
|
||||||
* @param pResponse
|
* @param pResponse
|
||||||
* The reponse injected by Spring.
|
* The reponse injected by Spring.
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
@@ -64,15 +72,19 @@ public class AccountController {
|
|||||||
*/
|
*/
|
||||||
@PutMapping("/changePassword")
|
@PutMapping("/changePassword")
|
||||||
public void changePassword(@RequestBody final PasswordWrapperDTO pPasswordWrapper,
|
public void changePassword(@RequestBody final PasswordWrapperDTO pPasswordWrapper,
|
||||||
final HttpServletRequest pRequest,
|
|
||||||
final HttpServletResponse pResponse,
|
final HttpServletResponse pResponse,
|
||||||
final Principal pPrincipal) throws IOException {
|
final Principal pPrincipal) {
|
||||||
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
|
int httpResponseCode = userService.getUserByPrincipal(pPrincipal)
|
||||||
if(connectedUser.isPresent()) {
|
.map(user -> {
|
||||||
accountService.changePassword(connectedUser.get(), pPasswordWrapper, pResponse);
|
try {
|
||||||
} else {
|
return accountService.changePassword(user, pPasswordWrapper);
|
||||||
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
} catch (IllegalArgumentException e) {
|
||||||
}
|
return HttpServletResponse.SC_BAD_REQUEST;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.orElse(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
|
||||||
|
pResponse.setStatus(httpResponseCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/signin")
|
@PostMapping("/signin")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.codiki.account;
|
package org.codiki.account;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -21,6 +22,8 @@ import org.codiki.core.security.CustomAuthenticationProvider;
|
|||||||
import org.codiki.core.services.FileUploadService;
|
import org.codiki.core.services.FileUploadService;
|
||||||
import org.codiki.core.services.UserService;
|
import org.codiki.core.services.UserService;
|
||||||
import org.codiki.core.utils.StringUtils;
|
import org.codiki.core.utils.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
@@ -30,22 +33,39 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AccountService {
|
public class AccountService {
|
||||||
|
/** Logger. */
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FileUploadService.class);
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CustomAuthenticationProvider authenticationProvider;
|
private CustomAuthenticationProvider authenticationProvider;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private FileUploadService fileUploadService;
|
private FileUploadService fileUploadService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ImageRepository imageRepository;
|
private ImageRepository imageRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param authenticationProvider
|
||||||
|
* @param userService
|
||||||
|
* @param userRepository
|
||||||
|
* @param fileUploadService
|
||||||
|
* @param imageRepository
|
||||||
|
*/
|
||||||
|
public AccountService(CustomAuthenticationProvider authenticationProvider,
|
||||||
|
UserService userService,
|
||||||
|
UserRepository userRepository,
|
||||||
|
FileUploadService fileUploadService,
|
||||||
|
ImageRepository imageRepository) {
|
||||||
|
this.authenticationProvider = authenticationProvider;
|
||||||
|
this.userService = userService;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.fileUploadService = fileUploadService;
|
||||||
|
this.imageRepository = imageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
public User authenticate(final User pUser) throws BadCredentialsException {
|
public User authenticate(final User pUser) throws BadCredentialsException {
|
||||||
final User user = userService.checkCredentials(pUser.getEmail(), pUser.getPassword());
|
final User user = userService.checkCredentials(pUser.getEmail(), pUser.getPassword());
|
||||||
|
|
||||||
@@ -54,23 +74,23 @@ public class AccountService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changePassword(final User pUser, final PasswordWrapperDTO pPasswordWrapper,
|
public int changePassword(final User pUser, final PasswordWrapperDTO pPasswordWrapper) throws IllegalArgumentException {
|
||||||
final HttpServletResponse pResponse) throws IOException {
|
int resultCode = HttpServletResponse.SC_NO_CONTENT;
|
||||||
|
|
||||||
if(pPasswordWrapper.getNewPassword().equals(pPasswordWrapper.getConfirmPassword())) {
|
if(pPasswordWrapper.getNewPassword().equals(pPasswordWrapper.getConfirmPassword())) {
|
||||||
// We fetch the connected user from database to get his hashed password
|
// We fetch the connected user from database to get his hashed password
|
||||||
final Optional<User> userFromDb = userRepository.findById(pUser.getId());
|
User user = userRepository.findById(pUser.getId())
|
||||||
if(userFromDb.isPresent() && StringUtils.compareHash(pPasswordWrapper.getOldPassword(),
|
.filter(u -> StringUtils.compareHash(pPasswordWrapper.getOldPassword(),
|
||||||
userFromDb.get().getPassword())) {
|
u.getPassword()))
|
||||||
userFromDb.get().setPassword(StringUtils.hashPassword(pPasswordWrapper.getNewPassword()));
|
.orElseThrow(IllegalArgumentException::new);
|
||||||
userRepository.save(userFromDb.get());
|
|
||||||
} else {
|
user.setPassword(StringUtils.hashPassword(pPasswordWrapper.getNewPassword()));
|
||||||
pResponse.sendError(HttpServletResponse.SC_FORBIDDEN,
|
userRepository.save(user);
|
||||||
"Le mot de passe saisi ne correspond pas au votre.");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
pResponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
|
resultCode = HttpServletResponse.SC_BAD_REQUEST;
|
||||||
"Le mot de passe saisi ne correspond pas au votre.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resultCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String uploadFile(final MultipartFile pFile, final HttpServletRequest pRequest,
|
public String uploadFile(final MultipartFile pFile, final HttpServletRequest pRequest,
|
||||||
@@ -93,7 +113,7 @@ public class AccountService {
|
|||||||
return avatarFileName;
|
return avatarFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadAvatar(final String pAvatarFileName) {
|
public Optional<Resource> loadAvatar(final String pAvatarFileName) {
|
||||||
return fileUploadService.loadAvatar(pAvatarFileName);
|
return fileUploadService.loadAvatar(pAvatarFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,25 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EntityScan("org.codiki")
|
@EntityScan("org.codiki.core.entities.persistence")
|
||||||
@EnableTransactionManagement
|
@EnableTransactionManagement
|
||||||
@EnableJpaRepositories("org.codiki")
|
@EnableJpaRepositories("org.codiki.core.repositories")
|
||||||
@PropertySource("classpath:application.properties")
|
@PropertySource("classpath:application.yml")
|
||||||
public class JpaConfiguration {
|
public class JpaConfiguration {
|
||||||
|
|
||||||
@Value("${spring.datasource.driverClassName}")
|
@Value("${spring.jpa.datasource.driverClassName}")
|
||||||
private String driverClassName;
|
private String driverClassName;
|
||||||
|
|
||||||
@Value("${spring.datasource.url}")
|
@Value("${spring.jpa.datasource.url}")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@Value("${spring.datasource.username}")
|
@Value("${spring.jpa.datasource.username}")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Value("${spring.datasource.password}")
|
@Value("${spring.jpa.datasource.password}")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Bean(name="dataSource")
|
@Bean(name = "dataSource")
|
||||||
public DataSource getDataSource() {
|
public DataSource getDataSource() {
|
||||||
return DataSourceBuilder.create()
|
return DataSourceBuilder.create()
|
||||||
.username(username)
|
.username(username)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.codiki.core.config;
|
package org.codiki.core.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@@ -9,6 +7,8 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
|||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This configuration class serves Angular app if the url isn't available in sprint REST module.
|
* This configuration class serves Angular app if the url isn't available in sprint REST module.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||||
@Override
|
@Override
|
||||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||||
AuthenticationException authException) throws IOException, ServletException {
|
AuthenticationException authException) throws IOException {
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,22 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|||||||
|
|
||||||
private static final String XSRF_REPOSITORY_HEADER_NAME = "X-XSRF-TOKEN";
|
private static final String XSRF_REPOSITORY_HEADER_NAME = "X-XSRF-TOKEN";
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CustomAuthenticationProvider authenticationProvider;
|
private CustomAuthenticationProvider authenticationProvider;
|
||||||
@Autowired
|
|
||||||
private RestAuthenticationEntryPoint authenticationEntryPoint;
|
private RestAuthenticationEntryPoint authenticationEntryPoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param authenticationProvider
|
||||||
|
* @param authenticationEntryPoint
|
||||||
|
*/
|
||||||
|
public SecurityConfiguration(CustomAuthenticationProvider authenticationProvider,
|
||||||
|
RestAuthenticationEntryPoint authenticationEntryPoint) {
|
||||||
|
this.authenticationProvider = authenticationProvider;
|
||||||
|
this.authenticationEntryPoint = authenticationEntryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
protected void configure(AuthenticationManagerBuilder auth) {
|
||||||
auth.authenticationProvider(authenticationProvider);
|
auth.authenticationProvider(authenticationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +53,9 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|||||||
.antMatchers(
|
.antMatchers(
|
||||||
"/api/images/uploadAvatar",
|
"/api/images/uploadAvatar",
|
||||||
"/api/images/myImages",
|
"/api/images/myImages",
|
||||||
"/api/posts/myPosts"
|
"/api/posts/myPosts",
|
||||||
|
"/api/account/changePassword",
|
||||||
|
"/api/account/"
|
||||||
).authenticated()
|
).authenticated()
|
||||||
.antMatchers(
|
.antMatchers(
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package org.codiki.core.services;
|
package org.codiki.core.services;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.apache.commons.lang.RandomStringUtils;
|
import org.apache.commons.lang.RandomStringUtils;
|
||||||
|
import org.codiki.core.utils.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -61,27 +65,41 @@ public class FileUploadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadAvatar(final String pAvatarFileName) {
|
public Optional<Resource> loadAvatar(final String pAvatarFileName) {
|
||||||
return loadImage(pAvatarFileName, folderProfileImages);
|
Optional<Resource> result = Optional.empty();
|
||||||
}
|
|
||||||
|
|
||||||
public Resource loadImage(final String pImageLink) {
|
|
||||||
return loadImage(pImageLink, folderImages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Resource loadImage(final String pImageLink, final String pFilePath) {
|
|
||||||
try {
|
try {
|
||||||
Path imageFile = Paths.get(pFilePath).resolve(pImageLink);
|
result = loadImage(pAvatarFileName, folderImages);
|
||||||
Resource imageResource = new UrlResource(imageFile.toUri());
|
} catch(final IOException ex) {
|
||||||
if(imageResource.exists() || imageResource.isReadable()) {
|
LOG.error("Unable to load avatar file {}", pAvatarFileName, ex);
|
||||||
return imageResource;
|
|
||||||
} else {
|
|
||||||
// TODO : Refactor exception
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
} catch(final MalformedURLException pEx) {
|
|
||||||
// TODO : Refactor exception
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Resource> loadImage(final String pImageLink) {
|
||||||
|
Optional<Resource> result = Optional.empty();
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = loadImage(pImageLink, folderImages);
|
||||||
|
} catch(final IOException ex) {
|
||||||
|
LOG.error("Unable to load image file {}", pImageLink, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Resource> loadImage(final String pImageLink, final String pFilePath) throws IOException {
|
||||||
|
Optional<Resource> result = Optional.empty();
|
||||||
|
|
||||||
|
Path imageFile = Paths.get(pFilePath).resolve(pImageLink);
|
||||||
|
Resource imageResource = new UrlResource(imageFile.toUri());
|
||||||
|
if(imageResource.exists() || imageResource.isReadable()) {
|
||||||
|
result = Optional.of(imageResource);
|
||||||
|
} else {
|
||||||
|
LOG.warn("Unknown file {}", imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import org.springframework.security.authentication.BadCredentialsException;
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.swing.text.html.Option;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
private static final String MSG_BAD_CREDENTIALS = "Adresse email ou mot de passe incorrect.";
|
private static final String MSG_BAD_CREDENTIALS = "Adresse email ou mot de passe incorrect.";
|
||||||
@@ -31,7 +33,13 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Optional<User> getUserByPrincipal(final Principal pPrincipal) {
|
public Optional<User> getUserByPrincipal(final Principal pPrincipal) {
|
||||||
SecurityContextHolder.getContext().getAuthentication();
|
Optional<User> result = Optional.empty();
|
||||||
return userRepository.findByEmail(pPrincipal.getName());
|
|
||||||
|
if(pPrincipal != null) {
|
||||||
|
SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
result = userRepository.findByEmail(pPrincipal.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,48 @@
|
|||||||
package org.codiki.images;
|
package org.codiki.images;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
import org.codiki.core.entities.dto.ImageDTO;
|
import org.codiki.core.entities.dto.ImageDTO;
|
||||||
import org.codiki.core.utils.StringUtils;
|
import org.codiki.core.utils.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/images")
|
@RequestMapping("/api/images")
|
||||||
public class ImageController {
|
public class ImageController {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ImageController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ImageController.class);
|
||||||
|
/** Image service. */
|
||||||
@Autowired
|
|
||||||
private ImageService imageService;
|
private ImageService imageService;
|
||||||
|
|
||||||
@PostMapping("/uploadAvatar")
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param imageService Image service.
|
||||||
|
*/
|
||||||
|
public ImageController(ImageService imageService) {
|
||||||
|
this.imageService = imageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/uploadAvatar")
|
||||||
public ResponseEntity<String> uploadAvatar(@RequestParam("file") MultipartFile pFile,
|
public ResponseEntity<String> uploadAvatar(@RequestParam("file") MultipartFile pFile,
|
||||||
final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Principal pPrincipal) {
|
final HttpServletRequest pRequest,
|
||||||
|
final HttpServletResponse pResponse,
|
||||||
|
final Principal pPrincipal) {
|
||||||
LOG.debug("Upload avatar.");
|
LOG.debug("Upload avatar.");
|
||||||
ResponseEntity<String> result;
|
ResponseEntity<String> result;
|
||||||
try {
|
try {
|
||||||
result = ResponseEntity.status(HttpStatus.OK)
|
result = ResponseEntity.ok(imageService.uploadAvatar(pFile, pResponse, pPrincipal));
|
||||||
.body(imageService.uploadAvatar(pFile, pRequest, pResponse, pPrincipal));
|
|
||||||
} catch(final Exception pEx) {
|
} catch(final Exception pEx) {
|
||||||
LOG.error("Error during avatar upload.", pEx);
|
LOG.error("Error during avatar upload.", pEx);
|
||||||
result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
|
result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
|
||||||
@@ -50,11 +53,14 @@ public class ImageController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile pFile,
|
public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile pFile,
|
||||||
final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Principal pPrincipal) {
|
final HttpServletResponse pResponse,
|
||||||
ResponseEntity<String> result;
|
final Principal pPrincipal) throws IOException {
|
||||||
|
ResponseEntity<String> result = null;
|
||||||
try {
|
try {
|
||||||
result = ResponseEntity.status(HttpStatus.OK)
|
result = ResponseEntity.status(HttpStatus.OK)
|
||||||
.body(imageService.uploadImage(pFile, pRequest, pResponse, pPrincipal));
|
.body(imageService.uploadImage(pFile, pPrincipal));
|
||||||
|
} catch(NoSuchElementException pEx) {
|
||||||
|
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
} catch(final Exception pEx) {
|
} catch(final Exception pEx) {
|
||||||
result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
|
result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
|
||||||
.body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + "."));
|
.body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + "."));
|
||||||
@@ -64,12 +70,16 @@ public class ImageController {
|
|||||||
|
|
||||||
@GetMapping("/loadAvatar/{avatarFileName}")
|
@GetMapping("/loadAvatar/{avatarFileName}")
|
||||||
public ResponseEntity<Resource> loadAvatar(@PathVariable("avatarFileName") final String pAvatarFileName) {
|
public ResponseEntity<Resource> loadAvatar(@PathVariable("avatarFileName") final String pAvatarFileName) {
|
||||||
return buildLoadImageResponse(imageService.loadAvatar(pAvatarFileName));
|
return imageService.loadAvatar(pAvatarFileName)
|
||||||
|
.map(this::buildLoadImageResponse)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{imageLink}")
|
@GetMapping("/{imageLink}")
|
||||||
public ResponseEntity<Resource> loadImage(@PathVariable("imageLink") final String pImageLink) {
|
public ResponseEntity<Resource> loadImage(@PathVariable("imageLink") final String pImageLink) {
|
||||||
return buildLoadImageResponse(imageService.loadImage(pImageLink));
|
return imageService.loadImage(pImageLink)
|
||||||
|
.map(this::buildLoadImageResponse)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<Resource> buildLoadImageResponse(final Resource pImageFile) {
|
private ResponseEntity<Resource> buildLoadImageResponse(final Resource pImageFile) {
|
||||||
@@ -79,13 +89,18 @@ public class ImageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/myImages")
|
@GetMapping("/myImages")
|
||||||
public List<ImageDTO> myImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
|
public List<ImageDTO> myImages(final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
|
||||||
final Principal pPrincipal) throws IOException {
|
List<ImageDTO> result = Collections.emptyList();
|
||||||
return imageService.getUserImages(pRequest, pResponse, pPrincipal);
|
try {
|
||||||
|
result = imageService.getUserImages(pPrincipal);
|
||||||
|
} catch (NoSuchElementException ex) {
|
||||||
|
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{imageLink}/details")
|
@GetMapping("/{imageLink}/details")
|
||||||
public ImageDTO getImageDetails(@PathVariable("imageLink") final String pImageLink, final HttpServletResponse pResponse) throws IOException {
|
public ResponseEntity<ImageDTO> getImageDetails(@PathVariable("imageLink") final String pImageLink) {
|
||||||
return imageService.getImageDetails(pImageLink, pResponse);
|
return ResponseEntity.of(imageService.getImageDetails(pImageLink));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
package org.codiki.images;
|
package org.codiki.images;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
import org.codiki.core.entities.dto.ImageDTO;
|
import org.codiki.core.entities.dto.ImageDTO;
|
||||||
import org.codiki.core.entities.persistence.Image;
|
import org.codiki.core.entities.persistence.Image;
|
||||||
import org.codiki.core.entities.persistence.User;
|
import org.codiki.core.entities.persistence.User;
|
||||||
@@ -18,27 +7,50 @@ import org.codiki.core.repositories.ImageRepository;
|
|||||||
import org.codiki.core.repositories.UserRepository;
|
import org.codiki.core.repositories.UserRepository;
|
||||||
import org.codiki.core.services.FileUploadService;
|
import org.codiki.core.services.FileUploadService;
|
||||||
import org.codiki.core.services.UserService;
|
import org.codiki.core.services.UserService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ImageService {
|
public class ImageService {
|
||||||
@Autowired
|
/** User service. */
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
/** User repository. */
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
/** File upload service. */
|
||||||
@Autowired
|
|
||||||
private FileUploadService fileUploadService;
|
private FileUploadService fileUploadService;
|
||||||
|
/** Image repository. */
|
||||||
@Autowired
|
|
||||||
private ImageRepository imageRepository;
|
private ImageRepository imageRepository;
|
||||||
|
|
||||||
public String uploadAvatar(final MultipartFile pFile, final HttpServletRequest pRequest,
|
/**
|
||||||
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
|
* Constructor.
|
||||||
|
* @param userService User service.
|
||||||
|
* @param userRepository User repository.
|
||||||
|
* @param fileUploadService File upload service.
|
||||||
|
* @param imageRepository Image repository.
|
||||||
|
*/
|
||||||
|
public ImageService(UserService userService,
|
||||||
|
UserRepository userRepository,
|
||||||
|
FileUploadService fileUploadService,
|
||||||
|
ImageRepository imageRepository) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.fileUploadService = fileUploadService;
|
||||||
|
this.imageRepository = imageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String uploadAvatar(final MultipartFile pFile,
|
||||||
|
final HttpServletResponse pResponse,
|
||||||
|
final Principal pPrincipal) throws IOException {
|
||||||
final String avatarFileName = fileUploadService.uploadProfileImage(pFile);
|
final String avatarFileName = fileUploadService.uploadProfileImage(pFile);
|
||||||
|
|
||||||
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
|
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
|
||||||
@@ -57,62 +69,44 @@ public class ImageService {
|
|||||||
return avatarFileName;
|
return avatarFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String uploadImage(final MultipartFile pFile, final HttpServletRequest pRequest,
|
public String uploadImage(final MultipartFile pFile, final Principal pPrincipal) {
|
||||||
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
|
|
||||||
final String imageFileName = fileUploadService.uploadImage(pFile);
|
final String imageFileName = fileUploadService.uploadImage(pFile);
|
||||||
|
|
||||||
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
|
userService.getUserByPrincipal(pPrincipal)
|
||||||
if(connectedUser.isPresent()) {
|
.map(User::getId)
|
||||||
final Optional<User> userFromDb = userRepository.findById(connectedUser.get().getId());
|
.map(userRepository::findById)
|
||||||
if(userFromDb.isPresent()) {
|
.orElseThrow(NoSuchElementException::new)
|
||||||
final Image image = new Image();
|
.ifPresent(user -> {
|
||||||
image.setLink(imageFileName);
|
final Image image = new Image();
|
||||||
image.setDate(new Date());
|
image.setLink(imageFileName);
|
||||||
image.setUser(userFromDb.get());
|
image.setDate(new Date());
|
||||||
imageRepository.save(image);
|
image.setUser(user);
|
||||||
} else {
|
imageRepository.save(image);
|
||||||
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
});
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageFileName;
|
return imageFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadAvatar(final String pAvatarFileName) {
|
public Optional<Resource> loadAvatar(final String pAvatarFileName) {
|
||||||
return fileUploadService.loadAvatar(pAvatarFileName);
|
return fileUploadService.loadAvatar(pAvatarFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadImage(final String pImageLink) {
|
public Optional<Resource> loadImage(final String pImageLink) {
|
||||||
return fileUploadService.loadImage(pImageLink);
|
return fileUploadService.loadImage(pImageLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ImageDTO> getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse,
|
public List<ImageDTO> getUserImages(final Principal pPrincipal) {
|
||||||
final Principal pPrincipal) throws IOException {
|
return userService.getUserByPrincipal(pPrincipal)
|
||||||
List<ImageDTO> result = new LinkedList<>();
|
.map(User::getId)
|
||||||
|
.map(imageRepository::getByUserId)
|
||||||
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
|
.orElseThrow(NoSuchElementException::new)
|
||||||
if(connectedUser.isPresent()) {
|
.stream()
|
||||||
result = imageRepository.getByUserId(connectedUser.get().getId())
|
.map(ImageDTO::new)
|
||||||
.stream().map(ImageDTO::new).collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
} else {
|
|
||||||
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageDTO getImageDetails(final String pImageLink, final HttpServletResponse pResponse) throws IOException {
|
public Optional<ImageDTO> getImageDetails(final String pImageLink) {
|
||||||
ImageDTO result = null;
|
return imageRepository.findByLink(pImageLink)
|
||||||
|
.map(ImageDTO::new);
|
||||||
final Optional<Image> imageFromDb = imageRepository.findByLink(pImageLink);
|
|
||||||
if(imageFromDb.isPresent()) {
|
|
||||||
result = new ImageDTO(imageFromDb.get());
|
|
||||||
} else {
|
|
||||||
pResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
#server.error.whitelabel.enabled=false
|
|
||||||
spring.datasource.driverClassName=org.postgresql.Driver
|
|
||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/codiki
|
|
||||||
spring.datasource.username=codiki
|
|
||||||
spring.datasource.password=P@ssword
|
|
||||||
|
|
||||||
# Disable feature detection by this undocumented parameter. Check the org.hibernate.engine.jdbc.internal.JdbcServiceImpl.configure method for more details.
|
|
||||||
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
|
|
||||||
|
|
||||||
# Because detection is disabled you have to set correct dialect by hand.
|
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
|
|
||||||
|
|
||||||
#logging.level.org.hibernate=DEBUG
|
|
||||||
|
|
||||||
spring.servlet.multipart.max-file-size=104857600
|
|
||||||
|
|
||||||
codiki.files.upload=/opt/codiki/pictures/tmp
|
|
||||||
codiki.files.profile-images=/opt/codiki/pictures/profiles
|
|
||||||
codiki.files.images=/opt/codiki/pictures/posts
|
|
||||||
logging.level.org.codiki=DEBUG
|
|
||||||
logging.path=/opt/codiki/logs
|
|
||||||
logging.file=codiki
|
|
||||||
cors.enabled=false
|
|
||||||
43
src/main/resources/application.yml
Normal file
43
src/main/resources/application.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
app:
|
||||||
|
name: Codiki
|
||||||
|
description: A wiki application.
|
||||||
|
|
||||||
|
codiki:
|
||||||
|
files:
|
||||||
|
upload: /opt/codiki/pictures/tmp
|
||||||
|
profile-images: /opt/codiki/pictures/profiles
|
||||||
|
images: /opt/codiki/pictures/posts
|
||||||
|
|
||||||
|
cors.enabled=false:
|
||||||
|
|
||||||
|
logging:
|
||||||
|
file: codiki
|
||||||
|
level:
|
||||||
|
org.codiki: DEBUG
|
||||||
|
# org.hibernate: DEBUG
|
||||||
|
path: /opt/codiki/logs
|
||||||
|
|
||||||
|
server:
|
||||||
|
# use-forward-headers=true
|
||||||
|
port: 8080
|
||||||
|
# ssl:
|
||||||
|
# key-store: /home/takiguchi/Developpement/Java/codiki/keystore.p12
|
||||||
|
# key-store-password: aaSrTBqpyRbwLhLi7iVwat2QWHCFQr3e4B9UkBpBPoUxH5UGdJpwenWAKZNTXwgww227CtBYUHogXivRdKpeyHPu2UafsPkGNuKgnDyzZfceFN5r3CxT2eTt
|
||||||
|
# keyStoreType: PKCS12
|
||||||
|
# keyAlias: tomcat
|
||||||
|
# whitelabel:
|
||||||
|
# enabled: false
|
||||||
|
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
datasource:
|
||||||
|
driverClassName: org.postgresql.Driver
|
||||||
|
url: jdbc:postgresql://localhost:5432/codiki
|
||||||
|
username: codiki
|
||||||
|
password: P@ssword
|
||||||
|
# Because detection is disabled you have to set correct dialect by hand.
|
||||||
|
database-platform: org.hibernate.dialect.PostgreSQL9Dialect
|
||||||
|
# Disable feature detection by this undocumented parameter.
|
||||||
|
# Check the org.hibernate.engine.jdbc.internal.JdbcServiceImpl.configure method for more details.
|
||||||
|
properties.hibernate.temp.use_jdbc_metadata_defaults: false
|
||||||
|
servlet.multipart.max-file-size: 104857600
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<property resource="application.properties" />
|
<property resource="application.yml" />
|
||||||
|
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<target>System.out</target>
|
<target>System.out</target>
|
||||||
|
|||||||
Reference in New Issue
Block a user