From 9ec252d0a48ad99527dddc2d4347530aafc83ac0 Mon Sep 17 00:00:00 2001 From: Hallvard Traetteberg <hal@ntnu.no> Date: Sat, 7 Apr 2018 23:44:27 +0200 Subject: [PATCH] An alternative web server implementation using Jersey and JAX-RS --- tdt4140-gr1800/pom.xml | 3 + tdt4140-gr1800/web.server.jaxrs/.classpath | 31 ++++ tdt4140-gr1800/web.server.jaxrs/.project | 23 +++ .../.settings/org.eclipse.jdt.core.prefs | 13 ++ .../.settings/org.eclipse.m2e.core.prefs | 4 + tdt4140-gr1800/web.server.jaxrs/README.md | 1 + tdt4140-gr1800/web.server.jaxrs/pom.xml | 158 ++++++++++++++++++ .../server/jaxrs/GeoObjectMapperProvider.java | 57 +++++++ .../gr1800/web/server/jaxrs/GeoService.java | 137 +++++++++++++++ .../web/server/jaxrs/GeoServiceConfig.java | 72 ++++++++ .../src/main/webapp/WEB-INF/web.xml | 30 ++++ .../gr1800/web/server/jaxrs/GeoServiceIT.java | 145 ++++++++++++++++ .../gr1800/web/server/Achterbroek-route.gpx | 96 +++++++++++ .../gr1800/web/server/Achterbroek-track.gpx | 100 +++++++++++ .../gr1800/web/server/geoLocations.json | 17 ++ .../tdt4140/gr1800/web/server/sample1.gpx | 26 +++ .../tdt4140/gr1800/web/server/GeoServlet.java | 56 +++---- 17 files changed, 937 insertions(+), 32 deletions(-) create mode 100644 tdt4140-gr1800/web.server.jaxrs/.classpath create mode 100644 tdt4140-gr1800/web.server.jaxrs/.project create mode 100644 tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.jdt.core.prefs create mode 100644 tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.m2e.core.prefs create mode 100644 tdt4140-gr1800/web.server.jaxrs/README.md create mode 100644 tdt4140-gr1800/web.server.jaxrs/pom.xml create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoObjectMapperProvider.java create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoService.java create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceConfig.java create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/main/webapp/WEB-INF/web.xml create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/test/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceIT.java create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-route.gpx create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-track.gpx create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/geoLocations.json create mode 100644 tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/sample1.gpx diff --git a/tdt4140-gr1800/pom.xml b/tdt4140-gr1800/pom.xml index 1102480..b6427db 100644 --- a/tdt4140-gr1800/pom.xml +++ b/tdt4140-gr1800/pom.xml @@ -15,6 +15,9 @@ <module>app.core</module> <module>FxMapControl</module> <module>app.ui</module> + <!-- <module>web.server</module> + --> + <module>web.server.jaxrs</module> </modules> </project> \ No newline at end of file diff --git a/tdt4140-gr1800/web.server.jaxrs/.classpath b/tdt4140-gr1800/web.server.jaxrs/.classpath new file mode 100644 index 0000000..d46faf1 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/.classpath @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" output="target/classes" path="src/main/java"> + <attributes> + <attribute name="optional" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="src" output="target/test-classes" path="src/test/java"> + <attributes> + <attribute name="optional" value="true"/> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> + <attributes> + <attribute name="maven.pomderived" value="true"/> + </attributes> + </classpathentry> + <classpathentry kind="output" path="target/classes"/> +</classpath> diff --git a/tdt4140-gr1800/web.server.jaxrs/.project b/tdt4140-gr1800/web.server.jaxrs/.project new file mode 100644 index 0000000..df8b92f --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/.project @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>tdt4140.gr1800.web.server.jaxrs.jaxrs.jaxrs</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.m2e.core.maven2Builder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.m2e.core.maven2Nature</nature> + </natures> +</projectDescription> diff --git a/tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.jdt.core.prefs b/tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..13b3428 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.m2e.core.prefs b/tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/tdt4140-gr1800/web.server.jaxrs/README.md b/tdt4140-gr1800/web.server.jaxrs/README.md new file mode 100644 index 0000000..1f4c88b --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/README.md @@ -0,0 +1 @@ +# web.server.jaxrs module - Web server providing a REST API to domain data, using JAX-RS implemented by Jersey diff --git a/tdt4140-gr1800/web.server.jaxrs/pom.xml b/tdt4140-gr1800/web.server.jaxrs/pom.xml new file mode 100644 index 0000000..16a2cb0 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/pom.xml @@ -0,0 +1,158 @@ +<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>tdt4140-gr1800.web.server.jaxrs</artifactId> + <packaging>war</packaging> + + <parent> + <groupId>tdt4140-gr1800</groupId> + <artifactId>tdt4140-gr1800</artifactId> + <version>0.0.1-SNAPSHOT</version> + </parent> + + <properties> + <jetty.version>9.4.8.v20171121</jetty.version> + <jersey.version>2.26</jersey.version> + </properties> + + <dependencies> + <dependency> + <groupId>tdt4140-gr1800</groupId> + <artifactId>tdt4140-gr1800.app.core</artifactId> + <version>0.0.1-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${jersey.version}</version> + </dependency> + + <!-- + Needed as of v. 2.26 + See https://stackoverflow.com/questions/44088493/jersey-stopped-working-with-injectionmanagerfactory-not-found + --> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${jersey.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${jersey.version}</version> + </dependency> + + <!-- to use Jackson as provider for JSON media type --> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + <version>2.26</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>2.9.3</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.9.3</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + <version>2.9.3</version> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>0.8.0</version> + <executions> + <execution> + <id>pre-unit-test</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>post-unit-test</id> + <phase>test</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-maven-plugin --> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-maven-plugin</artifactId> + <version>9.4.8.v20171121</version> + <configuration> + <webApp> + <contextPath>/jax-rs</contextPath> + </webApp> + <scanIntervalSeconds>0</scanIntervalSeconds> + <stopKey>stopMe</stopKey> + <stopPort>9966</stopPort> + <systemProperties> + <systemProperty> + <name>data.locations</name> + <value>file:${project.basedir}/src/test/resources/tdt4140/gr1800/web/server/geoLocations.json</value> + </systemProperty> + </systemProperties> + </configuration> + <executions> + <execution> + <id>start-jetty</id> + <phase>pre-integration-test</phase> + <goals> + <goal>start</goal> + </goals> + </execution> + <execution> + <id>stop-jetty</id> + <phase>post-integration-test</phase> + <goals> + <goal>stop</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> <!-- for starting jetty for integration tests --> + <version>2.16</version> + <executions> + <execution> + <id>integration-test</id> + <goals> + <goal>integration-test</goal> + </goals> + </execution> + <execution> + <id>verify</id> + <goals> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoObjectMapperProvider.java b/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoObjectMapperProvider.java new file mode 100644 index 0000000..b61ee97 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoObjectMapperProvider.java @@ -0,0 +1,57 @@ +package tdt4140.gr1800.web.server.jaxrs; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import tdt4140.gr1800.app.core.GeoLocation; +import tdt4140.gr1800.app.core.GeoLocations; +import tdt4140.gr1800.app.core.LatLong; +import tdt4140.gr1800.app.core.Person; +import tdt4140.gr1800.app.db.IdProvider; +import tdt4140.gr1800.app.json.GeoLocationDeserializer; +import tdt4140.gr1800.app.json.GeoLocationSerializer; +import tdt4140.gr1800.app.json.GeoLocationsDeserializer; +import tdt4140.gr1800.app.json.GeoLocationsSerializer; +import tdt4140.gr1800.app.json.LatLongDeserializer; +import tdt4140.gr1800.app.json.LatLongSerializer; +import tdt4140.gr1800.app.json.PersonDeserializer; +import tdt4140.gr1800.app.json.PersonSerializer; + +@Provider +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class GeoObjectMapperProvider implements ContextResolver<ObjectMapper> { + + private final ObjectMapper objectMapper; + private final PersonSerializer personSerializer; + + public GeoObjectMapperProvider() { + personSerializer = new PersonSerializer(); + final SimpleModule module = new SimpleModule() + .addSerializer(new LatLongSerializer()) + .addSerializer(new GeoLocationSerializer()) + .addSerializer(new GeoLocationsSerializer()) + .addSerializer(personSerializer) + .addDeserializer(LatLong.class, new LatLongDeserializer()) + .addDeserializer(GeoLocation.class, new GeoLocationDeserializer()) + .addDeserializer(GeoLocations.class, new GeoLocationsDeserializer()) + .addDeserializer(Person.class, new PersonDeserializer()); + objectMapper = new ObjectMapper() + .registerModule(module); + } + + public void setPersonIdProvider(final IdProvider<Person> idProvider) { + personSerializer.setIdProvider(idProvider); + } + + @Override + public ObjectMapper getContext(final Class<?> type) { + return objectMapper; + } +} diff --git a/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoService.java b/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoService.java new file mode 100644 index 0000000..164715e --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoService.java @@ -0,0 +1,137 @@ +package tdt4140.gr1800.web.server.jaxrs; + +import java.util.Collection; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Providers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import tdt4140.gr1800.app.core.GeoLocations; +import tdt4140.gr1800.app.core.Person; +import tdt4140.gr1800.app.db.IDbAccess; + +@Path("geo") +public class GeoService { + + @Inject + IDbAccess dbAccess; + + protected IDbAccess getDbAccess() { + return dbAccess; + } + + @Context + private Providers providers; + + @PostConstruct + protected void ensureIdProvider() { + final ContextResolver<ObjectMapper> resolver = providers.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE); + if (resolver instanceof GeoObjectMapperProvider) { + ((GeoObjectMapperProvider) resolver).setPersonIdProvider(getDbAccess().getPersonIdProvider()); + } + } + + // REST URL structure, according to https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/ + // persons/<id>/geoLocations/<num>/geoLocations/<num> + + // GET variants + // persons: Get all Person objects. Do we allow that? Should we return a list of <id> values or all the person entities (with some subset of properties) + // persons/<id>: Get a specific Person object + // persons/name or email: Get a specific Person object, with the provided name or email (with a '@') + // persons/<id>/geoLocations: Get all the GeoLocations objects, with (some subset of) properties + // persons/<id>/geoLocations/<num>: Get a specific GeoLocations object + // persons/<id>/geoLocations/<num>/geoLocations: Get all GeoLocation objects, with (some subset of) properties + // persons/<id>/geoLocations/<num>/geoLocations/<num>: Get a specific GeoLocation object + + @GET + @Path("/persons") + @Produces(MediaType.APPLICATION_JSON) + public Collection<Person> getPersons() { + // System.out.println("getPersons"); + return getDbAccess().getAllPersons(false); + } + + @GET + @Path("/persons/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Person getPerson(@PathParam("id") final String idOrKey) { + // System.out.println("getPerson: " + idOrKey); + Person person = null; + try { + final Integer num = Integer.valueOf(idOrKey); + person = getDbAccess().getPerson(num, false); + } catch (final NumberFormatException e) { + person = (idOrKey.contains("@") ? getDbAccess().getPersonByEmail(idOrKey, false) : getDbAccess().getPersonByName(idOrKey, false)); + } + return person; + } + + @GET + @Path("/persons/{id}/geoLocations") + @Produces(MediaType.APPLICATION_JSON) + public Collection<GeoLocations> getGeoLocations(@PathParam("id") final String idOrKey) throws RuntimeException { + // System.out.println("getGeoLocations: " + idOrKey); + final Person person = getDbAccess().getPerson(Integer.valueOf(idOrKey), false); + return getDbAccess().getGeoLocations(person, false); + } + + @GET + @Path("/persons/{id}/geoLocations/{num}") + @Produces(MediaType.APPLICATION_JSON) + public GeoLocations getGeoLocation(@PathParam("id") final int id, @PathParam("num") final int num) throws RuntimeException { + // System.out.println("getGeoLocation: " + id + "," + num); + final Person person = getDbAccess().getPerson(id, false); + final int geoNum = 0; + for (final GeoLocations geoLocations : getDbAccess().getGeoLocations(person, false)) { + if (geoNum == num) { + return geoLocations; + } + } + return null; + } + + // POST variants + // persons: Create a new Person object, with properties in the payload + // persons/<id>: Not allowed + // persons/<id>/geoLocations: Create a new GeoLocations object, with properties in the payload + // persons/<id>/geoLocations/<num>: Not allowed + // persons/<id>/geoLocations/<num>/geoLocations: Create a new GeoLocation object, with properties in the payload + // persons/<id>/geoLocations/<num>/geoLocations/<num>: Not allowed + + @POST + @Path("/persons") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Person createPerson(final Person person, @Context final Providers providers) { + final Person dbPerson = getDbAccess().createPerson(person.getName(), person.getEmail()); + return dbPerson; + } + + + // PUT variants + // persons: Not allowed + // persons/<id>: Update specific Person object + // persons/<id>/geoLocations: Not allowed + // persons/<id>/geoLocations/<num>: Update specific GeoLocations object + // persons/<id>/geoLocations/<num>/geoLocations: Not allowed + // persons/<id>/geoLocations/<num>/geoLocations/<num>: Update specific GeoLocation object + + // DELETE variants + // persons: Not allowed + // persons/<id>: Delete specific Person object + // persons/<id>/geoLocations: Delete all GeoLocations objects? + // persons/<id>/geoLocations/<num>: Delete specific GeoLocations object + // persons/<id>/geoLocations/<num>/geoLocations: Delete all GeoLocation objects? + // persons/<id>/geoLocations/<num>/geoLocations/<num>: Delete specific GeoLocation object +} diff --git a/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceConfig.java b/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceConfig.java new file mode 100644 index 0000000..703d0bf --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/main/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceConfig.java @@ -0,0 +1,72 @@ +package tdt4140.gr1800.web.server.jaxrs; + +import java.sql.SQLException; + +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; + +import tdt4140.gr1800.app.db.DbAccessImpl; +import tdt4140.gr1800.app.db.IDbAccess; + +public class GeoServiceConfig extends ResourceConfig { + + public GeoServiceConfig() { + + packages("tdt4140.gr1800.web.server.jaxrs"); + register(GeoService.class); + register(GeoObjectMapperProvider.class); + register(JacksonFeature.class); + + + // https://stackoverflow.com/questions/16216759/dependency-injection-with-jersey-2-0 + register(new AbstractBinder() { + @Override + protected void configure() { + bind(getDbAccess()).to(IDbAccess.class); + } + }); + } + + + private IDbAccess dbAccess; + + public IDbAccess getDbAccess() { + if (dbAccess == null) { + String dbConnectionUrl = "jdbc:hsqldb:mem:" + getClass().getName(); + final Object dbConnectionParam = getProperty("dbConnectionURL"); + if (dbConnectionParam != null) { + dbConnectionUrl = String.valueOf(dbConnectionParam); + } + try { + final DbAccessImpl dbAccess = new DbAccessImpl(dbConnectionUrl); + dbAccess.executeStatements("schema.sql", false); + this.dbAccess = dbAccess; + } catch (final SQLException e) { + throw new RuntimeException("Error when initializing database connection (" + dbConnectionUrl + "): " + e, e); + } + } + return dbAccess; + } + + // private ObjectMapper objectMapper; + // + // public ObjectMapper getObjectMapper() { + // if (objectMapper == null) { + // final PersonSerializer personSerializer = new PersonSerializer(); + // personSerializer.setIdProvider(getDbAccess().getPersonIdProvider()); + // final SimpleModule module = new SimpleModule() + // .addSerializer(new LatLongSerializer()) + // .addSerializer(new GeoLocationSerializer()) + // .addSerializer(new GeoLocationsSerializer()) + // .addSerializer(personSerializer) + // .addDeserializer(LatLong.class, new LatLongDeserializer()) + // .addDeserializer(GeoLocation.class, new GeoLocationDeserializer()) + // .addDeserializer(GeoLocations.class, new GeoLocationsDeserializer()) + // .addDeserializer(Person.class, new PersonDeserializer()); + // objectMapper = new ObjectMapper() + // .registerModule(module); + // } + // return objectMapper; + // } +} diff --git a/tdt4140-gr1800/web.server.jaxrs/src/main/webapp/WEB-INF/web.xml b/tdt4140-gr1800/web.server.jaxrs/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..0839542 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" + metadata-complete="false" version="3.1"> + + <!-- + https://stackoverflow.com/questions/38639425/basic-jersey-webservice-with-maven-jetty-plugin + --> + <servlet> + <servlet-name>Jersey REST Service</servlet-name> + <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> + <init-param> + <param-name>javax.ws.rs.Application</param-name> + <param-value>tdt4140.gr1800.web.server.jaxrs.GeoServiceConfig</param-value> + </init-param> + <!-- + <init-param> + <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> + <param-value>true</param-value> + </init-param> + <load-on-startup>1</load-on-startup> + --> + </servlet> + + <servlet-mapping> + <servlet-name>Jersey REST Service</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + +</web-app> diff --git a/tdt4140-gr1800/web.server.jaxrs/src/test/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceIT.java b/tdt4140-gr1800/web.server.jaxrs/src/test/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceIT.java new file mode 100644 index 0000000..6c7bc64 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/test/java/tdt4140/gr1800/web/server/jaxrs/GeoServiceIT.java @@ -0,0 +1,145 @@ +package tdt4140.gr1800.web.server.jaxrs; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.ValueNode; + +import tdt4140.gr1800.app.core.Person; + +public class GeoServiceIT { + + private ObjectMapper objectMapper; + + @Before + public void setUp() { + objectMapper = new GeoObjectMapperProvider().getContext(getClass()); + } + + private HttpURLConnection createUrlConnection(final String path) { + try { + return (HttpURLConnection) (new URL("http://localhost:8080/jax-rs/geo/" + path).openConnection()); + } catch (final MalformedURLException e) { + throw new RuntimeException(e); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + protected Collection<Person> getPersons(final ArrayNode arrayNode) throws JsonProcessingException { + final Collection<Person> persons = new ArrayList<Person>(); + for (final JsonNode childNode : arrayNode) { + persons.add(objectMapper.treeToValue(childNode, Person.class)); + } + return persons; + } + + protected int testPerson(final InputStream input, final Person person) throws Exception { + final JsonNode jsonTree = objectMapper.readTree(input); + Assert.assertTrue(jsonTree instanceof ObjectNode); + Assert.assertTrue(((ObjectNode) jsonTree).has("id")); + final JsonNode idNode = ((ObjectNode) jsonTree).get("id"); + Assert.assertTrue(idNode instanceof ValueNode); + final int id = (idNode instanceof NumericNode ? ((NumericNode) idNode).asInt() : Integer.valueOf(idNode.asText())); + Assert.assertTrue(isPerson(person, objectMapper.treeToValue(jsonTree, Person.class))); + return id; + } + + protected void testGetPerson(final int id, final Person person) throws Exception { + final HttpURLConnection con = createUrlConnection("persons/" + id); + con.setRequestMethod("GET"); + con.setDoInput(true); + try (InputStream conIn = con.getInputStream()) { + testPerson(conIn, person); + } + } + + protected boolean isPerson(final Person person, final Person conPerson) { + return person.getName().equals(conPerson.getName()) && person.getEmail().equals(conPerson.getEmail()); + } + + protected void testGetPersons(final Person... persons) throws Exception { + final HttpURLConnection con = createUrlConnection("persons"); + con.setRequestMethod("GET"); + con.setDoInput(true); + try (InputStream conIn = con.getInputStream()) { + final JsonNode jsonTree = objectMapper.readTree(conIn); + Assert.assertTrue(jsonTree instanceof ArrayNode); + final Collection<Person> conPersons = getPersons((ArrayNode) jsonTree); + outer: for (final Person person : persons) { + for (final Person conPerson : conPersons) { + if (isPerson(person, conPerson)) { + continue outer; + } + } + Assert.fail(); + } + } + } + + @Test + public void testCreatePersonGetPerson() throws Exception { + final Person person = new Person(); + person.setName("Hallvard"); + person.setEmail("hal@ntnu.no"); + final HttpURLConnection con = createUrlConnection("persons"); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.setDoInput(true); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + objectMapper.writeValue(out, person); + out.close(); + final byte[] bytes = out.toByteArray(); + con.setFixedLengthStreamingMode(bytes.length); + con.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + con.connect(); + try (OutputStream conOut = con.getOutputStream()) { + conOut.write(bytes); + } + try (InputStream conIn = con.getInputStream()) { + final int id = testPerson(conIn, person); + testGetPerson(id, person); + } + } + + @Test + public void testCreatePersonGetPersons() throws Exception { + final Person person = new Person(); + person.setName("Hallvard"); + person.setEmail("hallvard.traetteberg@gmail.com"); + final HttpURLConnection con = createUrlConnection("persons"); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.setDoInput(true); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + objectMapper.writeValue(out, person); + out.close(); + final byte[] bytes = out.toByteArray(); + con.setFixedLengthStreamingMode(bytes.length); + con.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + con.connect(); + try (OutputStream conOut = con.getOutputStream()) { + conOut.write(bytes); + } + try (InputStream conIn = con.getInputStream()) { + testGetPersons(person); + } + } +} diff --git a/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-route.gpx b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-route.gpx new file mode 100644 index 0000000..a006258 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-route.gpx @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<gpx + version="1.0" + creator="VB Net GPS: vermeiren-willy@pandora.be" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://www.topografix.com/GPX/1/0" + xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd"> + +<metadata> + <bounds minlat="51.396017" minlon="4.498043" maxlat="51.430402" maxlon="4.556108"/> +</metadata> + +<wpt lat="51.39709" lon="4.501519"> + <name>START</name> +</wpt> + +<rte> + <name>Achterbroek naar De Maatjes 13 km RT</name> + <rtept lat="51.39709" lon="4.501519"> + <name>1</name> + <cmt>1</cmt> + </rtept> + <rtept lat="51.398635" lon="4.502678"> + <name>2</name> + <cmt>2</cmt> + </rtept> + <rtept lat="51.405072" lon="4.498043"> + <name>3</name> + <cmt>3</cmt> + </rtept> + <rtept lat="51.417346" lon="4.515038"> + <name>4</name> + <cmt>4</cmt> + </rtept> + <rtept lat="51.41932" lon="4.513879"> + <name>5</name> + <cmt>5</cmt> + </rtept> + <rtept lat="51.421552" lon="4.523535"> + <name>6</name> + <cmt>6</cmt> + </rtept> + <rtept lat="51.423011" lon="4.522848"> + <name>7</name> + <cmt>7</cmt> + </rtept> + <rtept lat="51.423826" lon="4.5295"> + <name>8</name> + <cmt>8</cmt> + </rtept> + <rtept lat="51.427002" lon="4.528513"> + <name>9</name> + <cmt>9</cmt> + </rtept> + <rtept lat="51.430402" lon="4.552363"> + <name>10</name> + <cmt>10</cmt> + </rtept> + <rtept lat="51.426229" lon="4.555399"> + <name>11</name> + <cmt>11</cmt> + </rtept> + <rtept lat="51.426015" lon="4.554734"> + <name>12</name> + <cmt>12</cmt> + </rtept> + <rtept lat="51.423569" lon="4.556108"> + <name>13</name> + <cmt>13</cmt> + </rtept> + <rtept lat="51.41932" lon="4.546237"> + <name>14</name> + <cmt>14</cmt> + </rtept> + <rtept lat="51.410952" lon="4.534092"> + <name>15</name> + <cmt>15</cmt> + </rtept> + <rtept lat="51.406231" lon="4.543018"> + <name>16</name> + <cmt>16</cmt> + </rtept> + <rtept lat="51.396695" lon="4.515966"> + <name>17</name> + <cmt>17</cmt> + </rtept> + <rtept lat="51.396017" lon="4.515853"> + <name>18</name> + <cmt>18</cmt> + </rtept> + <rtept lat="51.397133" lon="4.502635"> + <name>19</name> + <cmt>19</cmt> + </rtept> +</rte> +</gpx> diff --git a/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-track.gpx b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-track.gpx new file mode 100644 index 0000000..755f740 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/Achterbroek-track.gpx @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<gpx + version="1.0" + creator="VB Net GPS: vermeiren-willy@pandora.be" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://www.topografix.com/GPX/1/0" + xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd"> + +<metadata> + <bounds minlat="51.396017" minlon="4.498043" maxlat="51.430402" maxlon="4.556108"/> +</metadata> + +<wpt lat="51.39709" lon="4.501519"> + <name>START</name> +</wpt> + +<trk> + <name>Achterbroek naar De Maatjes 13 km TR</name> + <trkseg> + <trkpt lat="51.39709" lon="4.501519"> + <name>1</name> + </trkpt> + <trkpt lat="51.397705" lon="4.501452"> + <name>2</name> + </trkpt> + <trkpt lat="51.398635" lon="4.502678"> + <name>3</name> + </trkpt> + <trkpt lat="51.404042" lon="4.498472"> + <name>4</name> + </trkpt> + <trkpt lat="51.405072" lon="4.498043"> + <name>5</name> + </trkpt> + <trkpt lat="51.405456" lon="4.499259"> + <name>6</name> + </trkpt> + <trkpt lat="51.417346" lon="4.515038"> + <name>7</name> + </trkpt> + <trkpt lat="51.41932" lon="4.513879"> + <name>8</name> + </trkpt> + <trkpt lat="51.421552" lon="4.523535"> + <name>9</name> + </trkpt> + <trkpt lat="51.423011" lon="4.522848"> + <name>10</name> + </trkpt> + <trkpt lat="51.423826" lon="4.5295"> + <name>11</name> + </trkpt> + <trkpt lat="51.427002" lon="4.528513"> + <name>12</name> + </trkpt> + <trkpt lat="51.427131" lon="4.534478"> + <name>13</name> + </trkpt> + <trkpt lat="51.426819" lon="4.53744"> + <name>14</name> + </trkpt> + <trkpt lat="51.430402" lon="4.552363"> + <name>15</name> + </trkpt> + <trkpt lat="51.426229" lon="4.555399"> + <name>16</name> + </trkpt> + <trkpt lat="51.426015" lon="4.554734"> + <name>17</name> + </trkpt> + <trkpt lat="51.423569" lon="4.556108"> + <name>18</name> + </trkpt> + <trkpt lat="51.423097" lon="4.553981"> + <name>19</name> + </trkpt> + <trkpt lat="51.41932" lon="4.546237"> + <name>20</name> + </trkpt> + <trkpt lat="51.410952" lon="4.534092"> + <name>21</name> + </trkpt> + <trkpt lat="51.406231" lon="4.543018"> + <name>22</name> + </trkpt> + <trkpt lat="51.396695" lon="4.515966"> + <name>23</name> + </trkpt> + <trkpt lat="51.396017" lon="4.515853"> + <name>24</name> + </trkpt> + <trkpt lat="51.396858" lon="4.506495"> + <name>25</name> + </trkpt> + <trkpt lat="51.397133" lon="4.502635"> + <name>26</name> + </trkpt> + </trkseg> +</trk> +</gpx> diff --git a/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/geoLocations.json b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/geoLocations.json new file mode 100644 index 0000000..bfda7a4 --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/geoLocations.json @@ -0,0 +1,17 @@ +[ + { + "name" : "1", + "locations" : [ + { "latitude" : "63", "longitude" : "10" }, + [ "63.1", "10.1" ] + ] + }, + { + "name" : "2", + "path" : "true", + "locations" : [ + { "latitude" : "64", "longitude" : "11" }, + [ "64.1", "11.1" ] + ] + } +] diff --git a/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/sample1.gpx b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/sample1.gpx new file mode 100644 index 0000000..79b840d --- /dev/null +++ b/tdt4140-gr1800/web.server.jaxrs/src/test/resources/tdt4140/gr1800/web/server/sample1.gpx @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no" ?> +<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" creator="Oregon 400t" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd"> + <metadata> + <link href="http://www.garmin.com"> + <text>Garmin International</text> + </link> + <time>2009-10-17T22:58:43Z</time> + </metadata> + <trk> + <name>Example GPX Document</name> + <trkseg> + <trkpt lat="47.644548" lon="-122.326897"> + <ele>4.46</ele> + <time>2009-10-17T18:37:26Z</time> + </trkpt> + <trkpt lat="47.644548" lon="-122.33"> + <ele>4.94</ele> + <time>2009-10-17T18:37:31Z</time> + </trkpt> + <trkpt lat="47.65" lon="-122.33"> + <ele>6.87</ele> + <time>2009-10-17T18:37:34Z</time> + </trkpt> + </trkseg> + </trk> +</gpx> diff --git a/tdt4140-gr1800/web.server/src/main/java/tdt4140/gr1800/web/server/GeoServlet.java b/tdt4140-gr1800/web.server/src/main/java/tdt4140/gr1800/web/server/GeoServlet.java index b4cf148..2fc6c0e 100644 --- a/tdt4140-gr1800/web.server/src/main/java/tdt4140/gr1800/web/server/GeoServlet.java +++ b/tdt4140-gr1800/web.server/src/main/java/tdt4140/gr1800/web/server/GeoServlet.java @@ -3,7 +3,9 @@ package tdt4140.gr1800.web.server; import java.io.IOException; import java.io.OutputStream; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import javax.servlet.ServletException; @@ -91,23 +93,33 @@ public class GeoServlet extends HttpServlet { // persons/<id>/geoLocations: Get all the GeoLocations objects, with (some subset of) properties // persons/<id>/geoLocations/<num>: Get a specific GeoLocations object // persons/<id>/geoLocations/<num>/geoLocations: Get all GeoLocation objects, with (some subset of) properties - // persons/<id>/geoLocations/<num>/geoLocations/<num>: Get a specific GeoLocations object + // persons/<id>/geoLocations/<num>/geoLocations/<num>: Get a specific GeoLocation object @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { - final HttpMethod method = new GetMethod(rootEntity); + doRequest(request, new GetMethod(rootEntity), false, true, response); + } + + protected void doRequest(final HttpServletRequest request, final HttpMethod method, final boolean jsonInput, final boolean jsonOutput, final HttpServletResponse response) throws ServletException, IOException { + final JsonNode jsonNode = (jsonInput ? objectMapper.readTree(request.getInputStream()) : null); + method.setPayload(jsonNode); final Object result = method.doMethod(getPathSegments(request)); if (result == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } else { - respondWithJson(response, result); + response.setStatus(HttpServletResponse.SC_OK); + if (jsonOutput) { + respondWithJson(response, result); + } else { + response.setStatus(HttpServletResponse.SC_OK); + } } } protected void respondWithJson(final HttpServletResponse response, final Object result) { response.setContentType("application/json"); - response.setStatus(HttpServletResponse.SC_OK); try (OutputStream output = response.getOutputStream()) { + response.setStatus(HttpServletResponse.SC_OK); objectMapper.writerWithDefaultPrettyPrinter().writeValue(output, result); } catch (final Exception e) { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); @@ -119,8 +131,10 @@ public class GeoServlet extends HttpServlet { if (path == null) { throw new ServletException("Path cannot be empty"); } - final Iterator<String> segments = Arrays.asList(path.split("\\/")).iterator(); - return segments; + final Collection<String> segments = new ArrayList<String>(Arrays.asList(path.split("\\/"))); + // remove empty segments + while (segments.remove("")); + return segments.iterator(); } // POST variants @@ -133,15 +147,7 @@ public class GeoServlet extends HttpServlet { @Override protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { - final HttpMethod method = new PostMethod(rootEntity); - final JsonNode jsonNode = objectMapper.readTree(request.getInputStream()); - method.setPayload(jsonNode); - final Object result = method.doMethod(getPathSegments(request)); - if (result == null) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } else { - respondWithJson(response, result); - } + doRequest(request, new PostMethod(rootEntity), true, true, response); } // PUT variants @@ -150,19 +156,11 @@ public class GeoServlet extends HttpServlet { // persons/<id>/geoLocations: Not allowed // persons/<id>/geoLocations/<num>: Update specific GeoLocations object // persons/<id>/geoLocations/<num>/geoLocations: Not allowed - // persons/<id>/geoLocations/<num>/geoLocations/<num>: Update specific GeoLocations object + // persons/<id>/geoLocations/<num>/geoLocations/<num>: Update specific GeoLocation object @Override protected void doPut(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { - final HttpMethod method = new PutMethod(rootEntity); - final JsonNode jsonNode = objectMapper.readTree(request.getInputStream()); - method.setPayload(jsonNode); - final Object result = method.doMethod(getPathSegments(request)); - if (result == null) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } else { - respondWithJson(response, result); - } + doRequest(request, new PutMethod(rootEntity), true, true, response); } // DELETE variants @@ -175,12 +173,6 @@ public class GeoServlet extends HttpServlet { @Override protected void doDelete(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { - final HttpMethod method = new DeleteMethod(rootEntity); - final Object result = method.doMethod(getPathSegments(request)); - if (result == null) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } else { - respondWithJson(response, result); - } + doRequest(request, new DeleteMethod(rootEntity), false, true, response); } } -- GitLab