Merge pull request #4493 from eugenp/update-mvc-java
update to spring 5
This commit is contained in:
commit
49c8323ba8
@ -7,10 +7,10 @@
|
|||||||
<name>spring-mvc-java</name>
|
<name>spring-mvc-java</name>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>parent-boot-1</artifactId>
|
<artifactId>parent-boot-2</artifactId>
|
||||||
<groupId>com.baeldung</groupId>
|
<groupId>com.baeldung</groupId>
|
||||||
<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>
|
||||||
@ -49,33 +49,26 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
<version>${jackson.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- web -->
|
<!-- web -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>javax.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
<version>${javax.servlet-api.version}</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>jstl</artifactId>
|
<artifactId>jstl</artifactId>
|
||||||
<version>${jstl.version}</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- AOP -->
|
<!-- AOP -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.aspectj</groupId>
|
<groupId>org.aspectj</groupId>
|
||||||
<artifactId>aspectjrt</artifactId>
|
<artifactId>aspectjrt</artifactId>
|
||||||
<version>${aspectj.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.aspectj</groupId>
|
<groupId>org.aspectj</groupId>
|
||||||
<artifactId>aspectjweaver</artifactId>
|
<artifactId>aspectjweaver</artifactId>
|
||||||
<version>${aspectj.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- common -->
|
<!-- common -->
|
||||||
@ -87,7 +80,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.htmlunit</groupId>
|
<groupId>net.sourceforge.htmlunit</groupId>
|
||||||
<artifactId>htmlunit</artifactId>
|
<artifactId>htmlunit</artifactId>
|
||||||
<version>${net.sourceforge.htmlunit}</version>
|
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<artifactId>commons-logging</artifactId>
|
<artifactId>commons-logging</artifactId>
|
||||||
@ -111,7 +103,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.jayway.jsonpath</groupId>
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
<artifactId>json-path</artifactId>
|
<artifactId>json-path</artifactId>
|
||||||
<version>${jsonpath.version}</version>
|
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -128,11 +119,6 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Validation -->
|
<!-- Validation -->
|
||||||
<dependency>
|
|
||||||
<groupId>javax.validation</groupId>
|
|
||||||
<artifactId>validation-api</artifactId>
|
|
||||||
<version>1.1.0.Final</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate</groupId>
|
<groupId>org.hibernate</groupId>
|
||||||
<artifactId>hibernate-validator</artifactId>
|
<artifactId>hibernate-validator</artifactId>
|
||||||
@ -170,13 +156,11 @@
|
|||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-resources-plugin</artifactId>
|
<artifactId>maven-resources-plugin</artifactId>
|
||||||
<version>${maven-resources-plugin.version}</version>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-war-plugin</artifactId>
|
<artifactId>maven-war-plugin</artifactId>
|
||||||
<version>${maven-war-plugin.version}</version>
|
|
||||||
<configuration>
|
<configuration>
|
||||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||||
</configuration>
|
</configuration>
|
||||||
@ -266,18 +250,14 @@
|
|||||||
</profiles>
|
</profiles>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- Spring -->
|
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
|
||||||
<thymeleaf.version>2.1.5.RELEASE</thymeleaf.version>
|
|
||||||
<jackson.version>2.9.4</jackson.version>
|
|
||||||
|
|
||||||
<!-- persistence -->
|
<!-- persistence -->
|
||||||
<hibernate.version>5.2.5.Final</hibernate.version>
|
<hibernate.version>5.2.5.Final</hibernate.version>
|
||||||
<mysql-connector-java.version>5.1.40</mysql-connector-java.version>
|
<mysql-connector-java.version>5.1.40</mysql-connector-java.version>
|
||||||
|
|
||||||
<!-- various -->
|
<!-- various -->
|
||||||
<hibernate-validator.version>5.4.1.Final</hibernate-validator.version>
|
<hibernate-validator.version>6.0.10.Final</hibernate-validator.version>
|
||||||
<javax.servlet-api.version>3.1.0</javax.servlet-api.version>
|
|
||||||
<jstl.version>1.2</jstl.version>
|
|
||||||
|
|
||||||
<!-- util -->
|
<!-- util -->
|
||||||
<guava.version>19.0</guava.version>
|
<guava.version>19.0</guava.version>
|
||||||
|
@ -3,7 +3,7 @@ package com.baeldung.app;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.web.support.SpringBootServletInitializer;
|
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
|
||||||
@EnableAutoConfiguration
|
@EnableAutoConfiguration
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
package com.baeldung.dialect;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import com.baeldung.processor.NameProcessor;
|
|
||||||
import org.thymeleaf.dialect.AbstractDialect;
|
|
||||||
import org.thymeleaf.processor.IProcessor;
|
|
||||||
|
|
||||||
public class CustomDialect extends AbstractDialect {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPrefix() {
|
|
||||||
return "custom";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<IProcessor> getProcessors() {
|
|
||||||
final Set<IProcessor> processors = new HashSet<IProcessor>();
|
|
||||||
processors.add(new NameProcessor());
|
|
||||||
return processors;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package com.baeldung.processor;
|
|
||||||
|
|
||||||
import org.thymeleaf.Arguments;
|
|
||||||
import org.thymeleaf.dom.Element;
|
|
||||||
import org.thymeleaf.processor.attr.AbstractTextChildModifierAttrProcessor;
|
|
||||||
|
|
||||||
public class NameProcessor extends AbstractTextChildModifierAttrProcessor {
|
|
||||||
|
|
||||||
public NameProcessor() {
|
|
||||||
super("name");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getText(final Arguments arguements, final Element elements, final String attributeName) {
|
|
||||||
return "Hello, " + elements.getAttributeValue(attributeName) + "!";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPrecedence() {
|
|
||||||
return 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,9 +1,7 @@
|
|||||||
package com.baeldung.spring.web.config;
|
package com.baeldung.spring.web.config;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import javax.servlet.ServletContext;
|
||||||
import java.util.Set;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import com.baeldung.dialect.CustomDialect;
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -13,17 +11,16 @@ import org.springframework.web.servlet.ViewResolver;
|
|||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
||||||
import org.springframework.web.servlet.view.JstlView;
|
import org.springframework.web.servlet.view.JstlView;
|
||||||
import org.thymeleaf.dialect.IDialect;
|
|
||||||
import org.thymeleaf.spring4.SpringTemplateEngine;
|
import org.thymeleaf.spring4.SpringTemplateEngine;
|
||||||
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
|
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
|
||||||
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
|
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
|
||||||
|
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
@Configuration
|
@Configuration
|
||||||
public class ClientWebConfig extends WebMvcConfigurerAdapter {
|
public class ClientWebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
public ClientWebConfig() {
|
public ClientWebConfig() {
|
||||||
super();
|
super();
|
||||||
@ -31,9 +28,11 @@ public class ClientWebConfig extends WebMvcConfigurerAdapter {
|
|||||||
|
|
||||||
// API
|
// API
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ServletContext ctx;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addViewControllers(final ViewControllerRegistry registry) {
|
public void addViewControllers(final ViewControllerRegistry registry) {
|
||||||
super.addViewControllers(registry);
|
|
||||||
|
|
||||||
registry.addViewController("/sample.html");
|
registry.addViewController("/sample.html");
|
||||||
}
|
}
|
||||||
@ -59,7 +58,7 @@ public class ClientWebConfig extends WebMvcConfigurerAdapter {
|
|||||||
@Bean
|
@Bean
|
||||||
@Description("Thymeleaf template resolver serving HTML 5")
|
@Description("Thymeleaf template resolver serving HTML 5")
|
||||||
public ServletContextTemplateResolver templateResolver() {
|
public ServletContextTemplateResolver templateResolver() {
|
||||||
final ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
|
final ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(ctx);
|
||||||
templateResolver.setPrefix("/WEB-INF/templates/");
|
templateResolver.setPrefix("/WEB-INF/templates/");
|
||||||
templateResolver.setSuffix(".html");
|
templateResolver.setSuffix(".html");
|
||||||
templateResolver.setTemplateMode("HTML5");
|
templateResolver.setTemplateMode("HTML5");
|
||||||
@ -71,9 +70,6 @@ public class ClientWebConfig extends WebMvcConfigurerAdapter {
|
|||||||
public SpringTemplateEngine templateEngine() {
|
public SpringTemplateEngine templateEngine() {
|
||||||
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||||
templateEngine.setTemplateResolver(templateResolver());
|
templateEngine.setTemplateResolver(templateResolver());
|
||||||
final Set<IDialect> dialects = new HashSet<>();
|
|
||||||
dialects.add(new CustomDialect());
|
|
||||||
templateEngine.setAdditionalDialects(dialects);
|
|
||||||
return templateEngine;
|
return templateEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
package com.baeldung.config;
|
|
||||||
|
|
||||||
import com.baeldung.web.controller.handlermapping.WelcomeController;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.web.servlet.ViewResolver;
|
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
|
||||||
import org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping;
|
|
||||||
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class ControllerClassNameHandlerMappingConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ViewResolver viewResolver() {
|
|
||||||
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
|
|
||||||
viewResolver.setPrefix("/");
|
|
||||||
viewResolver.setSuffix(".jsp");
|
|
||||||
return viewResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ControllerClassNameHandlerMapping controllerClassNameHandlerMapping() {
|
|
||||||
ControllerClassNameHandlerMapping controllerClassNameHandlerMapping = new ControllerClassNameHandlerMapping();
|
|
||||||
return controllerClassNameHandlerMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public WelcomeController welcome() {
|
|
||||||
return new WelcomeController();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package com.baeldung.handlermappings;
|
|
||||||
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
|
||||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
|
||||||
import org.springframework.test.context.web.WebAppConfiguration;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
||||||
import org.springframework.web.context.WebApplicationContext;
|
|
||||||
|
|
||||||
import com.baeldung.config.ControllerClassNameHandlerMappingConfig;
|
|
||||||
|
|
||||||
@RunWith(SpringJUnit4ClassRunner.class)
|
|
||||||
@WebAppConfiguration
|
|
||||||
@ContextConfiguration(classes = ControllerClassNameHandlerMappingConfig.class)
|
|
||||||
public class ControllerClassNameHandlerMappingIntegrationTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private WebApplicationContext webAppContext;
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setup() {
|
|
||||||
MockitoAnnotations.initMocks(this);
|
|
||||||
mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenControllerClassNameMapping_thenMappedOK() throws Exception {
|
|
||||||
mockMvc.perform(get("/welcome")).andExpect(status().isOk()).andExpect(view().name("welcome")).andDo(print());
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,6 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import com.gargoylesoftware.htmlunit.WebClient;
|
import com.gargoylesoftware.htmlunit.WebClient;
|
||||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||||
import com.gargoylesoftware.htmlunit.html.HtmlHeading1;
|
|
||||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||||
|
|
||||||
public class HtmlUnitWebScrapingLiveTest {
|
public class HtmlUnitWebScrapingLiveTest {
|
||||||
@ -37,7 +36,7 @@ public class HtmlUnitWebScrapingLiveTest {
|
|||||||
final HtmlAnchor latestPostLink = (HtmlAnchor) page.getByXPath(xpath).get(0);
|
final HtmlAnchor latestPostLink = (HtmlAnchor) page.getByXPath(xpath).get(0);
|
||||||
final HtmlPage postPage = latestPostLink.click();
|
final HtmlPage postPage = latestPostLink.click();
|
||||||
|
|
||||||
final List<HtmlHeading1> h1 = (List<HtmlHeading1>) postPage.getByXPath("//h1");
|
final List<Object> h1 = postPage.getByXPath("//h1");
|
||||||
|
|
||||||
Assert.assertTrue(h1.size() > 0);
|
Assert.assertTrue(h1.size() > 0);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package com.baeldung.htmlunit;
|
package com.baeldung.htmlunit;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.ViewResolver;
|
import org.springframework.web.servlet.ViewResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||||
import org.thymeleaf.spring4.SpringTemplateEngine;
|
import org.thymeleaf.spring4.SpringTemplateEngine;
|
||||||
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
|
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
|
||||||
@ -13,7 +17,10 @@ import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
|
|||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
@ComponentScan(basePackages = { "com.baeldung.web.controller" })
|
@ComponentScan(basePackages = { "com.baeldung.web.controller" })
|
||||||
public class TestConfig extends WebMvcConfigurerAdapter {
|
public class TestConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ServletContext ctx;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ViewResolver thymeleafViewResolver() {
|
public ViewResolver thymeleafViewResolver() {
|
||||||
@ -25,7 +32,7 @@ public class TestConfig extends WebMvcConfigurerAdapter {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ServletContextTemplateResolver templateResolver() {
|
public ServletContextTemplateResolver templateResolver() {
|
||||||
final ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
|
final ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(ctx);
|
||||||
templateResolver.setPrefix("/WEB-INF/templates/");
|
templateResolver.setPrefix("/WEB-INF/templates/");
|
||||||
templateResolver.setSuffix(".html");
|
templateResolver.setSuffix(".html");
|
||||||
templateResolver.setTemplateMode("HTML5");
|
templateResolver.setTemplateMode("HTML5");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user