commit
6c0a91ef6e
|
@ -128,6 +128,21 @@
|
||||||
<version>${awaitility.version}</version>
|
<version>${awaitility.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.rosuda.REngine</groupId>
|
||||||
|
<artifactId>Rserve</artifactId>
|
||||||
|
<version>${rserve.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.jbytecode</groupId>
|
||||||
|
<artifactId>RCaller</artifactId>
|
||||||
|
<version>${rcaller.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.renjin</groupId>
|
||||||
|
<artifactId>renjin-script-engine</artifactId>
|
||||||
|
<version>${renjin.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
|
@ -137,6 +152,13 @@
|
||||||
<url>http://repo.numericalmethod.com/maven/</url>
|
<url>http://repo.numericalmethod.com/maven/</url>
|
||||||
<layout>default</layout>
|
<layout>default</layout>
|
||||||
</repository>
|
</repository>
|
||||||
|
|
||||||
|
<!-- Needed for Renjin -->
|
||||||
|
<repository>
|
||||||
|
<id>bedatadriven</id>
|
||||||
|
<name>bedatadriven public repo</name>
|
||||||
|
<url>https://nexus.bedatadriven.com/content/groups/public/</url>
|
||||||
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
@ -153,6 +175,27 @@
|
||||||
<assertj.version>3.6.2</assertj.version>
|
<assertj.version>3.6.2</assertj.version>
|
||||||
<slf4j.version>1.7.25</slf4j.version>
|
<slf4j.version>1.7.25</slf4j.version>
|
||||||
<awaitility.version>3.0.0</awaitility.version>
|
<awaitility.version>3.0.0</awaitility.version>
|
||||||
|
<renjin.version>RELEASE</renjin.version>
|
||||||
|
<rcaller.version>3.0</rcaller.version>
|
||||||
|
<rserve.version>1.8.1</rserve.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<!-- Excludes FastR classes from compilations since they require GraalVM -->
|
||||||
|
<excludes>
|
||||||
|
<exclude>com/baeldung/r/FastRMean.java</exclude>
|
||||||
|
</excludes>
|
||||||
|
<testExcludes>
|
||||||
|
<exclude>com/baeldung/r/FastRMeanUnitTest.java</exclude>
|
||||||
|
</testExcludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FastR showcase.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
public class FastRMean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the customMean R function passing the given values as arguments.
|
||||||
|
*
|
||||||
|
* @param values the input to the mean script
|
||||||
|
* @return the result of the R script
|
||||||
|
*/
|
||||||
|
public double mean(int[] values) {
|
||||||
|
Context polyglot = Context.newBuilder()
|
||||||
|
.allowAllAccess(true)
|
||||||
|
.build();
|
||||||
|
String meanScriptContent = RUtils.getMeanScriptContent();
|
||||||
|
polyglot.eval("R", meanScriptContent);
|
||||||
|
Value rBindings = polyglot.getBindings("R");
|
||||||
|
Value rInput = rBindings.getMember("c")
|
||||||
|
.execute(values);
|
||||||
|
return rBindings.getMember("customMean")
|
||||||
|
.execute(rInput)
|
||||||
|
.asDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import com.github.rcaller.rstuff.RCaller;
|
||||||
|
import com.github.rcaller.rstuff.RCallerOptions;
|
||||||
|
import com.github.rcaller.rstuff.RCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RCaller showcase.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
public class RCallerMean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the customMean R function passing the given values as arguments.
|
||||||
|
*
|
||||||
|
* @param values the input to the mean script
|
||||||
|
* @return the result of the R script
|
||||||
|
* @throws IOException if any error occurs
|
||||||
|
* @throws URISyntaxException if any error occurs
|
||||||
|
*/
|
||||||
|
public double mean(int[] values) throws IOException, URISyntaxException {
|
||||||
|
String fileContent = RUtils.getMeanScriptContent();
|
||||||
|
RCode code = RCode.create();
|
||||||
|
code.addRCode(fileContent);
|
||||||
|
code.addIntArray("input", values);
|
||||||
|
code.addRCode("result <- customMean(input)");
|
||||||
|
RCaller caller = RCaller.create(code, RCallerOptions.create());
|
||||||
|
caller.runAndReturnResult("result");
|
||||||
|
return caller.getParser()
|
||||||
|
.getAsDoubleArray("result")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for loading the script.R content.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
public class RUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the script.R and returns its content as a string.
|
||||||
|
*
|
||||||
|
* @return the script.R content as a string
|
||||||
|
* @throws IOException if any error occurs
|
||||||
|
* @throws URISyntaxException if any error occurs
|
||||||
|
*/
|
||||||
|
static String getMeanScriptContent() throws IOException, URISyntaxException {
|
||||||
|
URI rScriptUri = RUtils.class.getClassLoader()
|
||||||
|
.getResource("script.R")
|
||||||
|
.toURI();
|
||||||
|
Path inputScript = Paths.get(rScriptUri);
|
||||||
|
return Files.lines(inputScript)
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
|
||||||
|
import org.renjin.script.RenjinScriptEngine;
|
||||||
|
import org.renjin.sexp.DoubleArrayVector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renjin showcase.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
public class RenjinMean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the customMean R function passing the given values as arguments.
|
||||||
|
*
|
||||||
|
* @param values the input to the mean script
|
||||||
|
* @return the result of the R script
|
||||||
|
* @throws IOException if any error occurs
|
||||||
|
* @throws URISyntaxException if any error occurs
|
||||||
|
* @throws ScriptException if any error occurs
|
||||||
|
*/
|
||||||
|
public double mean(int[] values) throws IOException, URISyntaxException, ScriptException {
|
||||||
|
RenjinScriptEngine engine = new RenjinScriptEngine();
|
||||||
|
String meanScriptContent = RUtils.getMeanScriptContent();
|
||||||
|
engine.put("input", values);
|
||||||
|
engine.eval(meanScriptContent);
|
||||||
|
DoubleArrayVector result = (DoubleArrayVector) engine.eval("customMean(input)");
|
||||||
|
return result.asReal();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import org.rosuda.REngine.REXPMismatchException;
|
||||||
|
import org.rosuda.REngine.REngineException;
|
||||||
|
import org.rosuda.REngine.Rserve.RConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rserve showcase.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
public class RserveMean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the Rserve istance listening on 127.0.0.1:6311 and invokes the
|
||||||
|
* customMean R function passing the given values as arguments.
|
||||||
|
*
|
||||||
|
* @param values the input to the mean script
|
||||||
|
* @return the result of the R script
|
||||||
|
* @throws REngineException if any error occurs
|
||||||
|
* @throws REXPMismatchException if any error occurs
|
||||||
|
*/
|
||||||
|
public double mean(int[] values) throws REngineException, REXPMismatchException {
|
||||||
|
RConnection c = new RConnection();
|
||||||
|
c.assign("input", values);
|
||||||
|
return c.eval("customMean(input)")
|
||||||
|
.asDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link FastRMean}.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
@Ignore
|
||||||
|
public class FastRMeanUnitTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object to test.
|
||||||
|
*/
|
||||||
|
private FastRMean fastrMean = new FastRMean();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link FastRMeanUnitTest#mean(int[])}.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void givenValues_whenMean_thenCorrect() {
|
||||||
|
int[] input = { 1, 2, 3, 4, 5 };
|
||||||
|
double result = fastrMean.mean(input);
|
||||||
|
Assert.assertEquals(3.0, result, 0.000001);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link RCallerMean}.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
@Ignore
|
||||||
|
public class RCallerMeanIntegrationTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object to test.
|
||||||
|
*/
|
||||||
|
private RCallerMean rcallerMean = new RCallerMean();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link RCallerMeanIntegrationTest#mean(int[])}.
|
||||||
|
*
|
||||||
|
* @throws ScriptException if an error occurs
|
||||||
|
* @throws URISyntaxException if an error occurs
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void givenValues_whenMean_thenCorrect() throws IOException, URISyntaxException {
|
||||||
|
int[] input = { 1, 2, 3, 4, 5 };
|
||||||
|
double result = rcallerMean.mean(input);
|
||||||
|
Assert.assertEquals(3.0, result, 0.000001);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link RenjinMean}.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
public class RenjinMeanUnitTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object to test.
|
||||||
|
*/
|
||||||
|
private RenjinMean renjinMean = new RenjinMean();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link RenjinMeanUnitTest#mean(int[])}.
|
||||||
|
*
|
||||||
|
* @throws ScriptException if an error occurs
|
||||||
|
* @throws URISyntaxException if an error occurs
|
||||||
|
* @throws IOException if an error occurs
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void givenValues_whenMean_thenCorrect() throws IOException, URISyntaxException, ScriptException {
|
||||||
|
int[] input = { 1, 2, 3, 4, 5 };
|
||||||
|
double result = renjinMean.mean(input);
|
||||||
|
Assert.assertEquals(3.0, result, 0.000001);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.baeldung.r;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.rosuda.REngine.REXPMismatchException;
|
||||||
|
import org.rosuda.REngine.REngineException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link RserveMean}.
|
||||||
|
*
|
||||||
|
* @author Donato Rimenti
|
||||||
|
*/
|
||||||
|
@Ignore
|
||||||
|
public class RserveMeanIntegrationTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object to test.
|
||||||
|
*/
|
||||||
|
private RserveMean rserveMean = new RserveMean();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for {@link RserveMeanIntegrationTest#mean(int[])}.
|
||||||
|
*
|
||||||
|
* @throws REXPMismatchException if an error occurs
|
||||||
|
* @throws REngineException if an error occurs
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void givenValues_whenMean_thenCorrect() throws REngineException, REXPMismatchException {
|
||||||
|
int[] input = { 1, 2, 3, 4, 5 };
|
||||||
|
double result = rserveMean.mean(input);
|
||||||
|
Assert.assertEquals(3.0, result, 0.000001);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
customMean <- function(vector) {
|
||||||
|
mean(vector)
|
||||||
|
}
|
|
@ -9,9 +9,9 @@
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.baeldung</groupId>
|
<groupId>com.baeldung</groupId>
|
||||||
<artifactId>parent-boot-1</artifactId>
|
<artifactId>parent-boot-2</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<relativePath>../../parent-boot-1</relativePath>
|
<relativePath>../../parent-boot-2</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-releasetrain</artifactId>
|
<artifactId>spring-data-releasetrain</artifactId>
|
||||||
<version>Hopper-SR10</version>
|
<version>Lovelace-SR16</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
@ -174,7 +174,7 @@
|
||||||
<start-class>com.baeldung.Application</start-class>
|
<start-class>com.baeldung.Application</start-class>
|
||||||
<spring.version>4.3.4.RELEASE</spring.version>
|
<spring.version>4.3.4.RELEASE</spring.version>
|
||||||
<httpclient.version>4.5.2</httpclient.version>
|
<httpclient.version>4.5.2</httpclient.version>
|
||||||
<spring-data-dynamodb.version>4.4.1</spring-data-dynamodb.version>
|
<spring-data-dynamodb.version>5.1.0</spring-data-dynamodb.version>
|
||||||
<aws-java-sdk-dynamodb.version>1.11.64</aws-java-sdk-dynamodb.version>
|
<aws-java-sdk-dynamodb.version>1.11.64</aws-java-sdk-dynamodb.version>
|
||||||
<bootstrap.version>3.3.7-1</bootstrap.version>
|
<bootstrap.version>3.3.7-1</bootstrap.version>
|
||||||
<sqlite4java.version>1.0.392</sqlite4java.version>
|
<sqlite4java.version>1.0.392</sqlite4java.version>
|
||||||
|
|
|
@ -44,8 +44,8 @@ public class DynamoDBConfig {
|
||||||
return new BasicAWSCredentials(amazonAWSAccessKey, amazonAWSSecretKey);
|
return new BasicAWSCredentials(amazonAWSAccessKey, amazonAWSSecretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "mvcHandlerMappingIntrospector")
|
@Bean(name = "mvcHandlerMappingIntrospectorCustom")
|
||||||
public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
|
public HandlerMappingIntrospector mvcHandlerMappingIntrospectorCustom() {
|
||||||
return new HandlerMappingIntrospector(context);
|
return new HandlerMappingIntrospector(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package com.baeldung.spring.data.dynamodb.repositories;
|
package com.baeldung.spring.data.dynamodb.repositories;
|
||||||
|
|
||||||
import com.baeldung.spring.data.dynamodb.model.ProductInfo;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
|
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import com.baeldung.spring.data.dynamodb.model.ProductInfo;
|
||||||
|
|
||||||
@EnableScan
|
@EnableScan
|
||||||
public interface ProductInfoRepository extends CrudRepository<ProductInfo, String> {
|
public interface ProductInfoRepository extends CrudRepository<ProductInfo, String> {
|
||||||
List<ProductInfo> findById(String id);
|
Optional<ProductInfo> findById(String id);
|
||||||
}
|
}
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -652,6 +652,7 @@
|
||||||
<module>spring-core</module>
|
<module>spring-core</module>
|
||||||
<module>spring-core-2</module>
|
<module>spring-core-2</module>
|
||||||
<module>spring-core-3</module>
|
<module>spring-core-3</module>
|
||||||
|
<module>spring-core-4</module>
|
||||||
<module>spring-cucumber</module>
|
<module>spring-cucumber</module>
|
||||||
|
|
||||||
<module>spring-data-rest</module>
|
<module>spring-data-rest</module>
|
||||||
|
@ -1157,6 +1158,7 @@
|
||||||
<module>spring-core</module>
|
<module>spring-core</module>
|
||||||
<module>spring-core-2</module>
|
<module>spring-core-2</module>
|
||||||
<module>spring-core-3</module>
|
<module>spring-core-3</module>
|
||||||
|
<module>spring-core-4</module>
|
||||||
<module>spring-cucumber</module>
|
<module>spring-cucumber</module>
|
||||||
|
|
||||||
<module>spring-data-rest</module>
|
<module>spring-data-rest</module>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.baeldung.springwithgroovy
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
|
||||||
|
import com.baeldung.springwithgroovy.SpringBootGroovyApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class SpringBootGroovyApplication {
|
||||||
|
static void main(String[] args) {
|
||||||
|
SpringApplication.run SpringBootGroovyApplication, args
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.baeldung.springwithgroovy.controller
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
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.PutMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
import com.baeldung.springwithgroovy.entity.Todo
|
||||||
|
import com.baeldung.springwithgroovy.service.TodoService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping('todo')
|
||||||
|
public class TodoController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TodoService todoService
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
List<Todo> getAllTodoList(){
|
||||||
|
todoService.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
Todo saveTodo(@RequestBody Todo todo){
|
||||||
|
todoService.saveTodo todo
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
Todo updateTodo(@RequestBody Todo todo){
|
||||||
|
todoService.updateTodo todo
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping('/{todoId}')
|
||||||
|
deleteTodo(@PathVariable Integer todoId){
|
||||||
|
todoService.deleteTodo todoId
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping('/{todoId}')
|
||||||
|
Todo getTodoById(@PathVariable Integer todoId){
|
||||||
|
todoService.findById todoId
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.baeldung.springwithgroovy.entity
|
||||||
|
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.GeneratedValue
|
||||||
|
import javax.persistence.GenerationType
|
||||||
|
import javax.persistence.Id
|
||||||
|
import javax.persistence.Table
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = 'todo')
|
||||||
|
class Todo {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
Integer id
|
||||||
|
|
||||||
|
@Column
|
||||||
|
String task
|
||||||
|
|
||||||
|
@Column
|
||||||
|
Boolean isCompleted
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.baeldung.springwithgroovy.repository
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
import com.baeldung.springwithgroovy.entity.Todo
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface TodoRepository extends JpaRepository<Todo, Integer> {}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.baeldung.springwithgroovy.service
|
||||||
|
|
||||||
|
import com.baeldung.springwithgroovy.entity.Todo
|
||||||
|
|
||||||
|
interface TodoService {
|
||||||
|
|
||||||
|
List<Todo> findAll()
|
||||||
|
|
||||||
|
Todo findById(Integer todoId)
|
||||||
|
|
||||||
|
Todo saveTodo(Todo todo)
|
||||||
|
|
||||||
|
Todo updateTodo(Todo todo)
|
||||||
|
|
||||||
|
Todo deleteTodo(Integer todoId)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.baeldung.springwithgroovy.service.impl
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
import com.baeldung.springwithgroovy.entity.Todo
|
||||||
|
import com.baeldung.springwithgroovy.repository.TodoRepository
|
||||||
|
import com.baeldung.springwithgroovy.service.TodoService
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TodoServiceImpl implements TodoService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TodoRepository todoRepository
|
||||||
|
|
||||||
|
@Override
|
||||||
|
List<Todo> findAll() {
|
||||||
|
todoRepository.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Todo findById(Integer todoId) {
|
||||||
|
todoRepository.findById todoId get()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Todo saveTodo(Todo todo){
|
||||||
|
todoRepository.save todo
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Todo updateTodo(Todo todo){
|
||||||
|
todoRepository.save todo
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Todo deleteTodo(Integer todoId){
|
||||||
|
todoRepository.deleteById todoId
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package com.baeldung.springwithgroovy
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.test.context.event.annotation.BeforeTestClass
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner
|
||||||
|
|
||||||
|
import com.baeldung.springwithgroovy.entity.Todo
|
||||||
|
|
||||||
|
import io.restassured.RestAssured
|
||||||
|
import io.restassured.response.Response
|
||||||
|
|
||||||
|
class TodoAppUnitTest {
|
||||||
|
static API_ROOT = 'http://localhost:8081/todo'
|
||||||
|
static readingTodoId
|
||||||
|
static writingTodoId
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
static void populateDummyData() {
|
||||||
|
Todo readingTodo = new Todo(task: 'Reading', isCompleted: false)
|
||||||
|
Todo writingTodo = new Todo(task: 'Writing', isCompleted: false)
|
||||||
|
|
||||||
|
final Response readingResponse =
|
||||||
|
RestAssured.given()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.body(readingTodo).post(API_ROOT)
|
||||||
|
|
||||||
|
Todo cookingTodoResponse = readingResponse.as Todo.class
|
||||||
|
readingTodoId = cookingTodoResponse.getId()
|
||||||
|
|
||||||
|
final Response writingResponse =
|
||||||
|
RestAssured.given()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.body(writingTodo).post(API_ROOT)
|
||||||
|
|
||||||
|
Todo writingTodoResponse = writingResponse.as Todo.class
|
||||||
|
writingTodoId = writingTodoResponse.getId()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenGetAllTodoList_thenOk(){
|
||||||
|
final Response response = RestAssured.get(API_ROOT)
|
||||||
|
|
||||||
|
assertEquals HttpStatus.OK.value(),response.getStatusCode()
|
||||||
|
assertTrue response.as(List.class).size() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenGetTodoById_thenOk(){
|
||||||
|
final Response response =
|
||||||
|
RestAssured.get("$API_ROOT/$readingTodoId")
|
||||||
|
|
||||||
|
assertEquals HttpStatus.OK.value(),response.getStatusCode()
|
||||||
|
Todo todoResponse = response.as Todo.class
|
||||||
|
assertEquals readingTodoId,todoResponse.getId()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenUpdateTodoById_thenOk(){
|
||||||
|
Todo todo = new Todo(id:readingTodoId, isCompleted: true)
|
||||||
|
final Response response =
|
||||||
|
RestAssured.given()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.body(todo).put(API_ROOT)
|
||||||
|
|
||||||
|
assertEquals HttpStatus.OK.value(),response.getStatusCode()
|
||||||
|
Todo todoResponse = response.as Todo.class
|
||||||
|
assertTrue todoResponse.getIsCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenDeleteTodoById_thenOk(){
|
||||||
|
final Response response =
|
||||||
|
RestAssured.given()
|
||||||
|
.delete("$API_ROOT/$writingTodoId")
|
||||||
|
|
||||||
|
assertEquals HttpStatus.OK.value(),response.getStatusCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenSaveTodo_thenOk(){
|
||||||
|
Todo todo = new Todo(task: 'Blogging', isCompleted: false)
|
||||||
|
final Response response =
|
||||||
|
RestAssured.given()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.body(todo).post(API_ROOT)
|
||||||
|
|
||||||
|
assertEquals HttpStatus.OK.value(),response.getStatusCode()
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.baeldung</groupId>
|
<groupId>com.baeldung</groupId>
|
||||||
<artifactId>parent-boot-1</artifactId>
|
<artifactId>parent-boot-2</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<relativePath>../../../parent-boot-1</relativePath>
|
<relativePath>../../../parent-boot-2</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.baeldung</groupId>
|
<groupId>com.baeldung</groupId>
|
||||||
<artifactId>parent-boot-1</artifactId>
|
<artifactId>parent-boot-2</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<relativePath>../../../parent-boot-1</relativePath>
|
<relativePath>../../../parent-boot-2</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
</project>
|
</project>
|
|
@ -0,0 +1,7 @@
|
||||||
|
## Spring Core
|
||||||
|
|
||||||
|
This module contains articles about core Spring functionality
|
||||||
|
|
||||||
|
## Relevant Articles:
|
||||||
|
|
||||||
|
- More articles: [[<-- prev]](/spring-core-3)
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>spring-core-4</artifactId>
|
||||||
|
<name>spring-core-4</name>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.baeldung</groupId>
|
||||||
|
<artifactId>parent-spring-5</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<relativePath>../parent-spring-5</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-context</artifactId>
|
||||||
|
<version>${spring.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-core</artifactId>
|
||||||
|
<version>${spring.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<version>${spring.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>${junit-jupiter.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>${junit-jupiter.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>${maven.surefire.version}</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.surefire.version>2.22.1</maven.surefire.version>
|
||||||
|
<annotation-api.version>1.3.2</annotation-api.version>
|
||||||
|
<spring.boot.version>2.2.2.RELEASE</spring.boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
public class Bar {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public Bar(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
public class Foo {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
public class InstanceBarFactory {
|
||||||
|
|
||||||
|
public Bar createInstance(String name) {
|
||||||
|
return new Bar(name);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
public class InstanceFooFactory {
|
||||||
|
|
||||||
|
public Foo createInstance() {
|
||||||
|
return new Foo();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
public class SingletonBarFactory {
|
||||||
|
|
||||||
|
private static final Bar INSTANCE = new Bar("unnamed");
|
||||||
|
|
||||||
|
public static Bar createInstance(String name) {
|
||||||
|
INSTANCE.setName(name);
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
public class SingletonFooFactory {
|
||||||
|
|
||||||
|
private static final Foo INSTANCE = new Foo();
|
||||||
|
|
||||||
|
public static Foo createInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@ContextConfiguration("/factorymethod/instance-bar-config.xml")
|
||||||
|
public class InstanceBarFactoryIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Bar instance;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenValidInstanceFactoryConfig_whenCreateInstance_thenNameIsCorrect() {
|
||||||
|
assertNotNull(instance);
|
||||||
|
assertEquals("someName", instance.getName());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@ContextConfiguration("/factorymethod/instance-foo-config.xml")
|
||||||
|
public class InstanceFooFactoryIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Foo foo;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenValidInstanceFactoryConfig_whenCreateFooInstance_thenInstanceIsNotNull() {
|
||||||
|
assertNotNull(foo);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@ContextConfiguration("/factorymethod/static-bar-config.xml")
|
||||||
|
public class SingletonBarFactoryIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Bar instance;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenValidStaticFactoryConfig_whenCreateInstance_thenNameIsCorrect() {
|
||||||
|
assertNotNull(instance);
|
||||||
|
assertEquals("someName", instance.getName());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.baeldung.factorymethod;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@ContextConfiguration("/factorymethod/static-foo-config.xml")
|
||||||
|
public class SingletonFooFactoryIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Foo singleton;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenValidStaticFactoryConfig_whenCreateInstance_thenInstanceIsNotNull() {
|
||||||
|
assertNotNull(singleton);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:util="http://www.springframework.org/schema/util"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||||
|
http://www.springframework.org/schema/util
|
||||||
|
http://www.springframework.org/schema/util/spring-util.xsd">
|
||||||
|
|
||||||
|
<bean id="instanceBarFactory"
|
||||||
|
class="com.baeldung.factorymethod.InstanceBarFactory" />
|
||||||
|
|
||||||
|
<bean id="bar"
|
||||||
|
factory-bean="instanceBarFactory"
|
||||||
|
factory-method="createInstance">
|
||||||
|
<constructor-arg value="someName" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
</beans>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:util="http://www.springframework.org/schema/util"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||||
|
http://www.springframework.org/schema/util
|
||||||
|
http://www.springframework.org/schema/util/spring-util.xsd">
|
||||||
|
|
||||||
|
<bean id="instanceFooFactory"
|
||||||
|
class="com.baeldung.factorymethod.InstanceFooFactory" />
|
||||||
|
|
||||||
|
<bean id="foo"
|
||||||
|
factory-bean="instanceFooFactory"
|
||||||
|
factory-method="createInstance" />
|
||||||
|
|
||||||
|
</beans>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:util="http://www.springframework.org/schema/util"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||||
|
http://www.springframework.org/schema/util
|
||||||
|
http://www.springframework.org/schema/util/spring-util.xsd">
|
||||||
|
|
||||||
|
<bean id="bar"
|
||||||
|
class="com.baeldung.factorymethod.SingletonBarFactory"
|
||||||
|
factory-method="createInstance">
|
||||||
|
<constructor-arg value="someName" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
</beans>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:util="http://www.springframework.org/schema/util"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||||
|
http://www.springframework.org/schema/util
|
||||||
|
http://www.springframework.org/schema/util/spring-util.xsd">
|
||||||
|
|
||||||
|
<bean id="foo"
|
||||||
|
class="com.baeldung.factorymethod.SingletonFooFactory"
|
||||||
|
factory-method="createInstance" />
|
||||||
|
|
||||||
|
</beans>
|
|
@ -1,8 +1,15 @@
|
||||||
package com.baeldung.app.controller;
|
package com.baeldung.app.controller;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
@ -10,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import com.baeldung.app.entity.Task;
|
import com.baeldung.app.entity.Task;
|
||||||
import com.baeldung.app.service.TaskService;
|
import com.baeldung.app.service.TaskService;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("api/tasks")
|
@RequestMapping("api/tasks")
|
||||||
public class TaskController {
|
public class TaskController {
|
||||||
|
@ -17,6 +26,9 @@ public class TaskController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private TaskService taskService;
|
private TaskService taskService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.GET)
|
@RequestMapping(method = RequestMethod.GET)
|
||||||
public ResponseEntity<Iterable<Task>> findAllTasks() {
|
public ResponseEntity<Iterable<Task>> findAllTasks() {
|
||||||
Iterable<Task> tasks = taskService.findAll();
|
Iterable<Task> tasks = taskService.findAll();
|
||||||
|
@ -30,4 +42,62 @@ public class TaskController {
|
||||||
|
|
||||||
return ResponseEntity.ok().body(tasks);
|
return ResponseEntity.ok().body(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of restricting specific endpoints to specific roles using @PreAuthorize.
|
||||||
|
*/
|
||||||
|
@GetMapping("/manager")
|
||||||
|
@PreAuthorize("hasRole('ROLE_MANAGER')")
|
||||||
|
public ResponseEntity<Iterable<Task>> getAlManagerTasks() {
|
||||||
|
Iterable<Task> tasks = taskService.findAll();
|
||||||
|
|
||||||
|
return ResponseEntity.ok().body(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of restricting specific endpoints to specific roles using SecurityContext.
|
||||||
|
*/
|
||||||
|
@GetMapping("/actuator")
|
||||||
|
public ResponseEntity<Iterable<Task>> getAlActuatorTasks() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ACTUATOR")))
|
||||||
|
{
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Task> tasks = taskService.findAll();
|
||||||
|
|
||||||
|
return ResponseEntity.ok().body(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of restricting specific endpoints to specific roles using UserDetailsService.
|
||||||
|
*/
|
||||||
|
@GetMapping("/admin")
|
||||||
|
public ResponseEntity<Iterable<Task>> getAlAdminTasks() {
|
||||||
|
if(userDetailsService != null) {
|
||||||
|
UserDetails details = userDetailsService.loadUserByUsername("pam");
|
||||||
|
if (details != null && details.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ADMIN"))) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Task> tasks = taskService.findAll();
|
||||||
|
|
||||||
|
return ResponseEntity.ok().body(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of restricting specific endpoints to specific roles using HttpServletRequest.
|
||||||
|
*/
|
||||||
|
@GetMapping("/admin2")
|
||||||
|
public ResponseEntity<Iterable<Task>> getAlAdminTasksUsingServlet(HttpServletRequest request) {
|
||||||
|
if (!request.isUserInRole("ROLE_ADMIN")) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Task> tasks = taskService.findAll();
|
||||||
|
|
||||||
|
return ResponseEntity.ok().body(tasks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.baeldung.customlogouthandler;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class CustomLogoutApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(CustomLogoutApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.baeldung.customlogouthandler;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import com.baeldung.customlogouthandler.web.CustomLogoutHandler;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class MvcConfiguration extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DataSource dataSource;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CustomLogoutHandler logoutHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
http.httpBasic()
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.GET, "/user/**")
|
||||||
|
.hasRole("USER")
|
||||||
|
.and()
|
||||||
|
.logout()
|
||||||
|
.logoutUrl("/user/logout")
|
||||||
|
.addLogoutHandler(logoutHandler)
|
||||||
|
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
|
||||||
|
.permitAll()
|
||||||
|
.and()
|
||||||
|
.csrf()
|
||||||
|
.disable()
|
||||||
|
.formLogin()
|
||||||
|
.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||||
|
auth.jdbcAuthentication()
|
||||||
|
.dataSource(dataSource)
|
||||||
|
.usersByUsernameQuery("select login, password, true from users where login=?")
|
||||||
|
.authoritiesByUsernameQuery("select login, role from users where login=?");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.baeldung.customlogouthandler.services;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.PersistenceContext;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baeldung.customlogouthandler.user.User;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserCache {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
private final ConcurrentMap<String, User> store = new ConcurrentHashMap<>(256);
|
||||||
|
|
||||||
|
public User getByUserName(String userName) {
|
||||||
|
return store.computeIfAbsent(userName, k -> entityManager.createQuery("from User where login=:login", User.class)
|
||||||
|
.setParameter("login", k)
|
||||||
|
.getSingleResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void evictUser(String userName) {
|
||||||
|
store.remove(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return store.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.baeldung.customlogouthandler.user;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
public class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String login;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogin() {
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogin(String login) {
|
||||||
|
this.login = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLanguage() {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLanguage(String language) {
|
||||||
|
this.language = language;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.baeldung.customlogouthandler.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
public class UserUtils {
|
||||||
|
|
||||||
|
public static String getAuthenticatedUserName() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext()
|
||||||
|
.getAuthentication();
|
||||||
|
return auth != null ? ((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.baeldung.customlogouthandler.web;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baeldung.customlogouthandler.services.UserCache;
|
||||||
|
import com.baeldung.customlogouthandler.user.UserUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CustomLogoutHandler implements LogoutHandler {
|
||||||
|
|
||||||
|
private final UserCache userCache;
|
||||||
|
|
||||||
|
public CustomLogoutHandler(UserCache userCache) {
|
||||||
|
this.userCache = userCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||||
|
String userName = UserUtils.getAuthenticatedUserName();
|
||||||
|
userCache.evictUser(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.baeldung.customlogouthandler.web;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
import com.baeldung.customlogouthandler.services.UserCache;
|
||||||
|
import com.baeldung.customlogouthandler.user.User;
|
||||||
|
import com.baeldung.customlogouthandler.user.UserUtils;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = "/user")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserCache userCache;
|
||||||
|
|
||||||
|
public UserController(UserCache userCache) {
|
||||||
|
this.userCache = userCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/language")
|
||||||
|
@ResponseBody
|
||||||
|
public String getLanguage() {
|
||||||
|
String userName = UserUtils.getAuthenticatedUserName();
|
||||||
|
User user = userCache.getByUserName(userName);
|
||||||
|
return user.getLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
spring.datasource.url=jdbc:postgresql://localhost:5432/test
|
||||||
|
spring.datasource.username=test
|
||||||
|
spring.datasource.password=test
|
||||||
|
|
||||||
|
spring.jpa.hibernate.ddl-auto=create
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.baeldung.customlogouthandler;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||||
|
import org.springframework.boot.web.server.LocalServerPort;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import org.springframework.test.context.jdbc.Sql;
|
||||||
|
import org.springframework.test.context.jdbc.SqlGroup;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
|
||||||
|
import com.baeldung.customlogouthandler.services.UserCache;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@SpringBootTest(classes = { CustomLogoutApplication.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@SqlGroup({ @Sql(value = "classpath:customlogouthandler/before.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), @Sql(value = "classpath:customlogouthandler/after.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) })
|
||||||
|
@TestPropertySource(locations="classpath:customlogouthandler/application.properties")
|
||||||
|
class CustomLogoutHandlerIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestRestTemplate restTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserCache userCache;
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenLogin_thenUseUserCache() {
|
||||||
|
// User cache should be empty on start
|
||||||
|
assertThat(userCache.size()).isEqualTo(0);
|
||||||
|
|
||||||
|
// Request using first login
|
||||||
|
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "pass")
|
||||||
|
.getForEntity(getLanguageUrl(), String.class);
|
||||||
|
|
||||||
|
assertThat(response.getBody()).contains("english");
|
||||||
|
|
||||||
|
// User cache must contain the user
|
||||||
|
assertThat(userCache.size()).isEqualTo(1);
|
||||||
|
|
||||||
|
// Getting the session cookie
|
||||||
|
HttpHeaders requestHeaders = new HttpHeaders();
|
||||||
|
requestHeaders.add("Cookie", response.getHeaders()
|
||||||
|
.getFirst(HttpHeaders.SET_COOKIE));
|
||||||
|
|
||||||
|
// Request with the session cookie
|
||||||
|
response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
|
||||||
|
assertThat(response.getBody()).contains("english");
|
||||||
|
|
||||||
|
// Logging out using the session cookies
|
||||||
|
response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
|
||||||
|
assertThat(response.getStatusCode()
|
||||||
|
.value()).isEqualTo(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenLogout_thenCacheIsEmpty() {
|
||||||
|
// User cache should be empty on start
|
||||||
|
assertThat(userCache.size()).isEqualTo(0);
|
||||||
|
|
||||||
|
// Request using first login
|
||||||
|
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "pass")
|
||||||
|
.getForEntity(getLanguageUrl(), String.class);
|
||||||
|
|
||||||
|
assertThat(response.getBody()).contains("english");
|
||||||
|
|
||||||
|
// User cache must contain the user
|
||||||
|
assertThat(userCache.size()).isEqualTo(1);
|
||||||
|
|
||||||
|
// Getting the session cookie
|
||||||
|
HttpHeaders requestHeaders = new HttpHeaders();
|
||||||
|
requestHeaders.add("Cookie", response.getHeaders()
|
||||||
|
.getFirst(HttpHeaders.SET_COOKIE));
|
||||||
|
|
||||||
|
// Logging out using the session cookies
|
||||||
|
response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
|
||||||
|
assertThat(response.getStatusCode()
|
||||||
|
.value()).isEqualTo(200);
|
||||||
|
|
||||||
|
// User cache must be empty now
|
||||||
|
// this is the reaction on custom logout filter execution
|
||||||
|
assertThat(userCache.size()).isEqualTo(0);
|
||||||
|
|
||||||
|
// Assert unauthorized request
|
||||||
|
response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, new HttpEntity<String>(requestHeaders), String.class);
|
||||||
|
assertThat(response.getStatusCode()
|
||||||
|
.value()).isEqualTo(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLanguageUrl() {
|
||||||
|
return "http://localhost:" + port + "/user/language";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLogoutUrl() {
|
||||||
|
return "http://localhost:" + port + "/user/logout";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
delete from users;
|
|
@ -0,0 +1,5 @@
|
||||||
|
spring.datasource.url=jdbc:postgresql://localhost:5432/test
|
||||||
|
spring.datasource.username=test
|
||||||
|
spring.datasource.password=test
|
||||||
|
|
||||||
|
spring.jpa.hibernate.ddl-auto=create
|
|
@ -0,0 +1 @@
|
||||||
|
insert into users (login, password, role, language) values ('user', '{noop}pass', 'ROLE_USER', 'english');
|
Loading…
Reference in New Issue