DATAREST-414 - Polishing.
Extracted groups of dependencies into auto-activated profiles to make more visible which dependencies belong to which store. Simplified dependency setup where possible. General polishing in the test cases added.
This commit is contained in:
111
pom.xml
111
pom.xml
@@ -65,6 +65,59 @@
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<profiles>
|
||||
|
||||
<profile>
|
||||
<id>jpa</id>
|
||||
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-jpa</artifactId>
|
||||
<version>${springdata.jpa}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-entitymanager</artifactId>
|
||||
<version>${hibernate.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>mongodb</id>
|
||||
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb</artifactId>
|
||||
<version>${springdata.mongodb}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
@@ -74,64 +127,6 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-entitymanager</artifactId>
|
||||
<version>${hibernate.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-jpa</artifactId>
|
||||
<version>${springdata.jpa}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb</artifactId>
|
||||
<version>${springdata.mongodb}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
<version>${springdata.neo4j}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-gemfire</artifactId>
|
||||
<version>${springdata.gemfire}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-solr</artifactId>
|
||||
<version>${springdata.solr}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-cassandra</artifactId>
|
||||
<version>${springdata.cassandra}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -83,9 +83,9 @@
|
||||
<!-- JSON patch -->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.fge</groupId>
|
||||
<artifactId>json-patch</artifactId>
|
||||
<version>1.7</version>
|
||||
<groupId>com.github.fge</groupId>
|
||||
<artifactId>json-patch</artifactId>
|
||||
<version>1.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Optional store specifics -->
|
||||
@@ -116,6 +116,41 @@
|
||||
|
||||
<profiles>
|
||||
|
||||
<profile>
|
||||
<id>neo4j</id>
|
||||
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
<version>${springdata.neo4j}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>gemfire</id>
|
||||
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-gemfire</artifactId>
|
||||
<version>${springdata.gemfire}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>cassandra</id>
|
||||
|
||||
@@ -125,6 +160,13 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-cassandra</artifactId>
|
||||
<version>${springdata.cassandra}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.cassandra</groupId>
|
||||
<artifactId>cassandra-all</artifactId>
|
||||
@@ -148,14 +190,6 @@
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.cassandra</groupId>
|
||||
<artifactId>cassandra-all</artifactId>
|
||||
@@ -178,7 +212,7 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- It appears that something else pulls antlr-runtime up to 3.5, which breaks cassandra -->
|
||||
<!-- Declare Antlr locally to favor this one as Solr pulls in 3.5 which breaks Cassandra -->
|
||||
<dependency>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>antlr-runtime</artifactId>
|
||||
@@ -193,12 +227,6 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
|
||||
<artifactId>concurrentlinkedhashmap-lru</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
@@ -210,6 +238,14 @@
|
||||
</activation>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-solr</artifactId>
|
||||
<version>${springdata.solr}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.solr</groupId>
|
||||
<artifactId>solr-core</artifactId>
|
||||
|
||||
@@ -17,14 +17,15 @@ package org.springframework.data.rest.webmvc.cassandra;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.Session;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.thrift.transport.TTransportException;
|
||||
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
|
||||
import org.junit.BeforeClass;
|
||||
import org.springframework.data.rest.webmvc.CommonWebTests;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.Session;
|
||||
|
||||
/**
|
||||
* Base class for testing with an embedded cassandra database
|
||||
*
|
||||
@@ -35,11 +36,12 @@ public abstract class AbstractCassandraIntegrationTest extends CommonWebTests {
|
||||
/**
|
||||
* The session connected to the system keyspace.
|
||||
*/
|
||||
protected Session systemSession;
|
||||
Session systemSession;
|
||||
|
||||
/**
|
||||
* The {@link com.datastax.driver.core.Cluster} that's connected to Cassandra.
|
||||
*/
|
||||
protected Cluster cluster;
|
||||
Cluster cluster;
|
||||
|
||||
/**
|
||||
* Launch an embedded Cassandra instance
|
||||
@@ -54,6 +56,7 @@ public abstract class AbstractCassandraIntegrationTest extends CommonWebTests {
|
||||
}
|
||||
|
||||
public AbstractCassandraIntegrationTest() {
|
||||
|
||||
// check cluster
|
||||
if (cluster == null) {
|
||||
cluster = Cluster.builder()//
|
||||
@@ -67,5 +70,4 @@ public abstract class AbstractCassandraIntegrationTest extends CommonWebTests {
|
||||
systemSession = cluster.connect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.springframework.data.rest.webmvc.cassandra;
|
||||
|
||||
public class CassandraProperties {
|
||||
class CassandraProperties {
|
||||
|
||||
public static final String HOSTNAME = "localhost";
|
||||
public static final int PORT = 9142;
|
||||
static final String HOSTNAME = "localhost";
|
||||
static final int PORT = 9142;
|
||||
}
|
||||
|
||||
@@ -32,17 +32,20 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Integration tests for Cassandra repositories
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@ContextConfiguration(classes = CassandraRepoConfig.class)
|
||||
public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
|
||||
@Autowired private EmployeeRepository repository;
|
||||
@Autowired EmployeeRepository repository;
|
||||
ObjectMapper mapper;
|
||||
|
||||
@Override
|
||||
protected Iterable<String> expectedRootLinkRels() {
|
||||
@@ -50,30 +53,32 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given that Spring Data Cassandra can leave behind persistent artifacts, need to clean out ALL entities
|
||||
* before launching a given test case.
|
||||
*
|
||||
* @throws ConfigurationException
|
||||
* @throws IOException
|
||||
* @throws TTransportException
|
||||
* @throws InterruptedException
|
||||
* Given that Spring Data Cassandra can leave behind persistent artifacts, need to clean out ALL entities before
|
||||
* launching a given test case.
|
||||
*/
|
||||
@Before
|
||||
public void cleanoutDatabase() throws ConfigurationException, IOException, TTransportException, InterruptedException {
|
||||
|
||||
repository.deleteAll();
|
||||
this.repository.deleteAll();
|
||||
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-414
|
||||
*/
|
||||
@Test
|
||||
public void create() throws Exception {
|
||||
|
||||
Link employeeLink = client.discoverUnique("employees");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
Employee employee = new Employee();
|
||||
employee.setId("789");
|
||||
employee.setFirstName("Bilbo");
|
||||
employee.setLastName("Baggins");
|
||||
employee.setTitle("burgler");
|
||||
|
||||
String bilboString = mapper.writeValueAsString(employee);
|
||||
|
||||
MockHttpServletResponse response = postAndGet(employeeLink, bilboString, MediaType.APPLICATION_JSON);
|
||||
@@ -84,10 +89,11 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* After inserting data directly through a Spring Data Cassandra repository, verify Spring Data REST can
|
||||
* fetch the resources through hypermedia.
|
||||
* After inserting data directly through a Spring Data Cassandra repository, verify Spring Data REST can fetch the
|
||||
* resources through hypermedia.
|
||||
*
|
||||
* @throws Exception
|
||||
* @see DATAREST-414
|
||||
*/
|
||||
@Test
|
||||
public void findAllEmployees() throws Exception {
|
||||
@@ -97,6 +103,7 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
employee1.setFirstName("Frodo");
|
||||
employee1.setLastName("Baggins");
|
||||
employee1.setTitle("ring bearer");
|
||||
|
||||
repository.save(employee1);
|
||||
|
||||
Employee employee2 = new Employee();
|
||||
@@ -104,22 +111,23 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
employee2.setFirstName("Samwise");
|
||||
employee2.setLastName("Gamgee");
|
||||
employee2.setTitle("ring bearer");
|
||||
|
||||
repository.save(employee2);
|
||||
|
||||
Link employeesLink = client.discoverUnique("employees");
|
||||
|
||||
client.follow(employeesLink)
|
||||
.andExpect(status().isOk())
|
||||
client.follow(employeesLink).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._embedded.employees[*].firstName", hasItems("Samwise", "Frodo")))
|
||||
.andExpect(jsonPath("$._embedded.employees[*].lastName", hasItems("Gamgee", "Baggins")))
|
||||
.andExpect(jsonPath("$._embedded.employees[*].title", hasItems("ring bearer", "ring bearer")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify some basic creation (POST), updating (PATH) and replacing (PUT) functionality of Spring
|
||||
* Data Cassandra through Spring Data REST.
|
||||
* Verify some basic creation (POST), updating (PATH) and replacing (PUT) functionality of Spring Data Cassandra
|
||||
* through Spring Data REST.
|
||||
*
|
||||
* @throws Exception
|
||||
* @see DATAREST-414
|
||||
*/
|
||||
@Test
|
||||
public void createAnEmployee() throws Exception {
|
||||
@@ -129,7 +137,7 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
employee.setFirstName("Frodo");
|
||||
employee.setLastName("Baggins");
|
||||
employee.setTitle("ring bearer");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
String employeeString = mapper.writeValueAsString(employee);
|
||||
|
||||
Link employeeLink = client.discoverUnique("employees");
|
||||
@@ -143,8 +151,8 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
assertThat(newlyMintedEmployee.getLastName(), equalTo(employee.getLastName()));
|
||||
assertThat(newlyMintedEmployee.getTitle(), equalTo(employee.getTitle()));
|
||||
|
||||
MockHttpServletResponse response2 = patchAndGet(
|
||||
newlyMintedEmployeeLink, "{\"firstName\": \"Bilbo\"}", MediaType.APPLICATION_JSON);
|
||||
MockHttpServletResponse response2 = patchAndGet(newlyMintedEmployeeLink, "{\"firstName\": \"Bilbo\"}",
|
||||
MediaType.APPLICATION_JSON);
|
||||
|
||||
Link refurbishedEmployeeLink = client.assertHasLinkWithRel("self", response2);
|
||||
Employee refurbishedEmployee = mapper.readValue(response2.getContentAsString(), Employee.class);
|
||||
@@ -153,8 +161,8 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
assertThat(refurbishedEmployee.getLastName(), equalTo(employee.getLastName()));
|
||||
assertThat(refurbishedEmployee.getTitle(), equalTo(employee.getTitle()));
|
||||
|
||||
MockHttpServletResponse response3 = putAndGet(
|
||||
refurbishedEmployeeLink, "{\"lastName\": \"Jr.\"}", MediaType.APPLICATION_JSON);
|
||||
MockHttpServletResponse response3 = putAndGet(refurbishedEmployeeLink, "{\"lastName\": \"Jr.\"}",
|
||||
MediaType.APPLICATION_JSON);
|
||||
|
||||
Employee lastEmployee = mapper.readValue(response3.getContentAsString(), Employee.class);
|
||||
|
||||
@@ -167,6 +175,7 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
* Verify that creating (POST) and then updating (PATCH) a resource only updates the sub-set of fields.
|
||||
*
|
||||
* @throws Exception
|
||||
* @see DATAREST-414
|
||||
*/
|
||||
@Test
|
||||
public void createThenPatch() throws Exception {
|
||||
@@ -176,7 +185,7 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
employee.setFirstName("Frodo");
|
||||
employee.setLastName("Baggins");
|
||||
employee.setTitle("ring bearer");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
String employeeString = mapper.writeValueAsString(employee);
|
||||
|
||||
Link employeeLink = client.discoverUnique("employees");
|
||||
@@ -190,8 +199,8 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
assertThat(newlyMintedEmployee.getLastName(), equalTo(employee.getLastName()));
|
||||
assertThat(newlyMintedEmployee.getTitle(), equalTo(employee.getTitle()));
|
||||
|
||||
MockHttpServletResponse response2 = patchAndGet(
|
||||
newlyMintedEmployeeLink, "{\"firstName\": \"Bilbo\"}", MediaType.APPLICATION_JSON);
|
||||
MockHttpServletResponse response2 = patchAndGet(newlyMintedEmployeeLink, "{\"firstName\": \"Bilbo\"}",
|
||||
MediaType.APPLICATION_JSON);
|
||||
|
||||
Employee refurbishedEmployee = mapper.readValue(response2.getContentAsString(), Employee.class);
|
||||
|
||||
@@ -201,13 +210,12 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that first creating (POST) and then replacing (PUT) a resource with a subset of fields only causes
|
||||
* the subset of fields to be changed inside Cassandra.
|
||||
*
|
||||
* NOTE: Cassandra doesn't handle nulls like traditional databases and Spring Data Cassandra ignores
|
||||
* null fields.
|
||||
* Verify that first creating (POST) and then replacing (PUT) a resource with a subset of fields only causes the
|
||||
* subset of fields to be changed inside Cassandra. NOTE: Cassandra doesn't handle nulls like traditional databases
|
||||
* and Spring Data Cassandra ignores {@literal null} fields.
|
||||
*
|
||||
* @throws Exception
|
||||
* @see DATAREST-414
|
||||
*/
|
||||
@Test
|
||||
public void createThenPut() throws Exception {
|
||||
@@ -217,7 +225,7 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
employee.setFirstName("Frodo");
|
||||
employee.setLastName("Baggins");
|
||||
employee.setTitle("ring bearer");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
String employeeString = mapper.writeValueAsString(employee);
|
||||
|
||||
Link employeeLink = client.discoverUnique("employees");
|
||||
@@ -231,8 +239,8 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
assertThat(newlyMintedEmployee.getLastName(), equalTo(employee.getLastName()));
|
||||
assertThat(newlyMintedEmployee.getTitle(), equalTo(employee.getTitle()));
|
||||
|
||||
MockHttpServletResponse response2 = putAndGet(
|
||||
newlyMintedEmployeeLink, "{\"firstName\": \"Bilbo\"}", MediaType.APPLICATION_JSON);
|
||||
MockHttpServletResponse response2 = putAndGet(newlyMintedEmployeeLink, "{\"firstName\": \"Bilbo\"}",
|
||||
MediaType.APPLICATION_JSON);
|
||||
|
||||
Employee refurbishedEmployee = mapper.readValue(response2.getContentAsString(), Employee.class);
|
||||
|
||||
@@ -243,5 +251,4 @@ public class CassandraWebTests extends AbstractCassandraIntegrationTest {
|
||||
assertThat(refurbishedEmployee.getLastName(), equalTo(employee.getLastName()));
|
||||
assertThat(refurbishedEmployee.getTitle(), equalTo(employee.getTitle()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.cassandra;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import org.springframework.data.cassandra.mapping.PrimaryKey;
|
||||
import org.springframework.data.cassandra.mapping.Table;
|
||||
|
||||
@@ -23,12 +22,12 @@ import org.springframework.data.cassandra.mapping.Table;
|
||||
* Simple domain object
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Table
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Employee {
|
||||
|
||||
@PrimaryKey private String id;
|
||||
private @PrimaryKey String id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String title;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
Reference in New Issue
Block a user