From 86cb062646413f23f8b89a739945747fed9d2e80 Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Sun, 10 Jan 2016 16:25:45 +0100
Subject: [PATCH] added linked iface + generic service impl

added log files to gitignore
added log file appender to cmd log4j config
set proper context root for rest project via maven
added linked interface for rest model classes
added generic request service for rest project that sets base uri automatically
renamed topic to topicdefinition in rest project
added motd to vm bootstrap for login info
removed mongod.conf configuration
added ui topic routes and template stubs
---
 .gitignore                                    |  3 +-
 ma-impl.sublime-workspace                     | 73 +++++--------------
 vipra-cmd/src/main/resources/log4j2.xml       |  9 +++
 vipra-rest/.classpath                         |  6 ++
 .../org.eclipse.wst.common.component          |  2 +-
 vipra-rest/pom.xml                            |  4 +
 .../java/de/vipra/rest/model/Article.java     |  2 +-
 .../main/java/de/vipra/rest/model/Linked.java |  4 +-
 .../{Topic.java => TopicDefinition.java}      |  2 +-
 .../vipra/rest/resource/ArticleResource.java  | 14 ++--
 .../de/vipra/rest/resource/TopicResource.java | 26 +++----
 .../de/vipra/rest/service/ArticleService.java | 37 +---------
 .../java/de/vipra/rest/service/Service.java   | 53 ++++++++++++++
 .../de/vipra/rest/service/TopicService.java   | 24 ++++++
 .../app/components/dynamic-high-charts.js     |  1 -
 vipra-ui/app/models/topic.js                  |  5 ++
 vipra-ui/app/router.js                        |  4 +
 vipra-ui/app/routes/topics/list.js            |  9 +++
 vipra-ui/app/templates/articles/new.hbs       |  1 -
 vipra-ui/app/templates/index.hbs              |  6 +-
 vipra-ui/app/templates/topics.hbs             |  1 +
 vipra-ui/app/templates/topics/list.hbs        |  1 +
 vipra-ui/app/templates/topics/show.hbs        |  0
 vipra-ui/tests/unit/models/topic-test.js      | 12 +++
 vipra-ui/tests/unit/routes/topics-test.js     | 11 +++
 .../main/java/de/vipra/util/model/Topic.java  | 34 ---------
 .../de/vipra/util/model/TopicDefinition.java  | 22 +++---
 .../vipra/util/service/DatabaseService.java   |  7 +-
 vm/bootstrap.sh                               |  1 +
 vm/config/mongod.conf                         | 41 -----------
 vm/config/motd                                |  8 ++
 31 files changed, 218 insertions(+), 205 deletions(-)
 rename vipra-rest/src/main/java/de/vipra/rest/model/{Topic.java => TopicDefinition.java} (85%)
 create mode 100644 vipra-rest/src/main/java/de/vipra/rest/service/Service.java
 create mode 100644 vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java
 create mode 100644 vipra-ui/app/models/topic.js
 create mode 100644 vipra-ui/app/routes/topics/list.js
 delete mode 100644 vipra-ui/app/templates/articles/new.hbs
 create mode 100644 vipra-ui/app/templates/topics.hbs
 create mode 100644 vipra-ui/app/templates/topics/list.hbs
 create mode 100644 vipra-ui/app/templates/topics/show.hbs
 create mode 100644 vipra-ui/tests/unit/models/topic-test.js
 create mode 100644 vipra-ui/tests/unit/routes/topics-test.js
 delete mode 100644 vipra-util/src/main/java/de/vipra/util/model/Topic.java
 delete mode 100644 vm/config/mongod.conf
 create mode 100644 vm/config/motd

diff --git a/.gitignore b/.gitignore
index f7500c5f..0aecefcd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
+*.log
 .vagrant/
-vm/webapps/
\ No newline at end of file
+vm/webapps/
diff --git a/ma-impl.sublime-workspace b/ma-impl.sublime-workspace
index 9e358ffc..400b2e04 100644
--- a/ma-impl.sublime-workspace
+++ b/ma-impl.sublime-workspace
@@ -275,14 +275,6 @@
 	},
 	"buffers":
 	[
-		{
-			"file": "vm/bootstrap.sh",
-			"settings":
-			{
-				"buffer_size": 3519,
-				"line_ending": "Unix"
-			}
-		}
 	],
 	"build_system": "",
 	"build_system_choices":
@@ -462,15 +454,24 @@
 	"expanded_folders":
 	[
 		"/home/eike/repos/master/ma-impl",
-		"/home/eike/repos/master/ma-impl/vm"
+		"/home/eike/repos/master/ma-impl/vipra-cmd"
 	],
 	"file_history":
 	[
-		"/usr/share/applications/defaults.list",
+		"/home/eike/repos/master/ma-impl/vipra-cmd.sh",
+		"/home/eike/repos/master/ma-impl/vipra-cmd/build2.xml",
+		"/home/eike/repos/master/ma-impl/vipra-ui/README.md",
+		"/home/eike/repos/master/ma-impl/vipra-ui/bower.json",
+		"/home/eike/repos/testasd/bower.json",
+		"/home/eike/repos/master/ma-impl/vipra-ui2/package.json",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/components/dynamic-high-charts.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/articles/list.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/package.json",
+		"/home/eike/repos/master/ma-impl/vipra-ui/node_modules/ember-highcharts/package.json",
+		"/home/eike/repos/master/ma-impl/vm/bootstrap.sh",
 		"/home/eike/repos/master/ma-impl/vm/config/initd-mongod",
 		"/home/eike/repos/master/ma-impl/vm/webapps/vipra-rest/WEB-INF/web.xml",
 		"/core",
-		"/home/eike/repos/master/ma-impl/vm/bootstrap.sh",
 		"/home/eike/repos/master/ma-impl/vm/config/environment",
 		"/home/eike/repos/master/ma-impl/Vagrantfile",
 		"/home/eike/repos/master/ma-impl/vm/config/initd-tomcat",
@@ -584,16 +585,7 @@
 		"/home/eike/.duplicity-exclude",
 		"/home/eike/Repositories/fu/ss15/ki/exercise-08/task2a.pl",
 		"/home/eike/Repositories/fu/ss15/ki/exercise-08/games.nkb",
-		"/home/eike/Repositories/fu/ss15/ki/exercise-08/exercise-08.tex",
-		"/home/eike/Repositories/fu/ss15/ki/exercise-08/task01.pl",
-		"/home/eike/Repositories/latex-templates/invoice.tex",
-		"/home/eike/Repositories/niels_website/Rechnungen/exercise-01.tex",
-		"/home/eike/OwnCloud/NiELS/todo-2015-06-13.txt",
-		"/home/eike/Repositories/fu/ss15/bfm/exercise-06/ex06.txt",
-		"/home/eike/Repositories/fu/ss15/bfm/exercise-04/ex04.txt",
-		"/home/eike/Repositories/fu/ss15/bfm/exercise-05/ex05.txt",
-		"/home/eike/Repositories/fu/ss15/ki/exercise-08/native.pl",
-		"/home/eike/Repositories/fu/ss15/ki/exercise-08/birds.nkb"
+		"/home/eike/Repositories/fu/ss15/ki/exercise-08/exercise-08.tex"
 	],
 	"find":
 	{
@@ -642,7 +634,6 @@
 		"case_sensitive": false,
 		"find_history":
 		[
-			"gedit.desktop",
 			"00:00Z\" },",
 			"{ \"$date\": ",
 			".000+0000",
@@ -769,7 +760,8 @@
 			"intended",
 			"redirect_intended",
 			"amount-in",
-			"'"
+			"'",
+			"!important"
 		],
 		"highlight": true,
 		"in_selection": false,
@@ -777,7 +769,6 @@
 		"regex": false,
 		"replace_history":
 		[
-			"sublime_text.desktop",
 			"00:00Z\",",
 			"",
 			"Z",
@@ -904,7 +895,8 @@
 			"@append$1",
 			"survey",
 			"SurveysController",
-			""
+			"",
+			"/assets"
 		],
 		"reverse": false,
 		"show_context": true,
@@ -915,37 +907,8 @@
 	"groups":
 	[
 		{
-			"selected": 0,
 			"sheets":
 			[
-				{
-					"buffer": 0,
-					"file": "vm/bootstrap.sh",
-					"semi_transient": false,
-					"settings":
-					{
-						"buffer_size": 3519,
-						"regions":
-						{
-						},
-						"selection":
-						[
-							[
-								933,
-								933
-							]
-						],
-						"settings":
-						{
-							"syntax": "Packages/ShellScript/Shell-Unix-Generic.tmLanguage"
-						},
-						"translation.x": 0.0,
-						"translation.y": 0.0,
-						"zoom_level": 1.0
-					},
-					"stack_index": 0,
-					"type": "text"
-				}
 			]
 		}
 	],
@@ -996,7 +959,7 @@
 	"project": "ma-impl.sublime-project",
 	"replace":
 	{
-		"height": 66.0
+		"height": 46.0
 	},
 	"save_all_on_build": true,
 	"select_file":
diff --git a/vipra-cmd/src/main/resources/log4j2.xml b/vipra-cmd/src/main/resources/log4j2.xml
index 2fc755c1..f2c17f38 100644
--- a/vipra-cmd/src/main/resources/log4j2.xml
+++ b/vipra-cmd/src/main/resources/log4j2.xml
@@ -4,10 +4,19 @@
 		<Console name="Console" target="SYSTEM_OUT">
 			<PatternLayout pattern="%highlight{%-5level - %msg%n}{FATAL=red,ERROR=red,WARN=red,INFO=normal,DEBUG=normal,TRACE=normal}" />
 		</Console>
+		<File name="File" fileName="vipra-cmd.log" append="false">
+			<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n" />
+			<Filters>
+				<ThresholdFilter level="INFO" onMatch="DENY" onMismatch="NEUTRAL"/>
+				<ThresholdFilter level="DEBUG" onMatch="DENY" onMismatch="NEUTRAL"/>
+				<ThresholdFilter level="TRACE" onMatch="DENY" onMismatch="NEUTRAL"/>
+			</Filters>
+		</File>
 	</Appenders>
 	<Loggers>
 		<Root level="ERROR">
 			<AppenderRef ref="Console" />
+			<AppenderRef ref="File" />
 		</Root>
 		<Logger name="shellout" level="INFO" />
 	</Loggers>
diff --git a/vipra-rest/.classpath b/vipra-rest/.classpath
index 9045b682..3d2bb266 100644
--- a/vipra-rest/.classpath
+++ b/vipra-rest/.classpath
@@ -22,5 +22,11 @@
 			<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 kind="output" path="target/classes"/>
 </classpath>
diff --git a/vipra-rest/.settings/org.eclipse.wst.common.component b/vipra-rest/.settings/org.eclipse.wst.common.component
index a844e2a0..8f4686f8 100644
--- a/vipra-rest/.settings/org.eclipse.wst.common.component
+++ b/vipra-rest/.settings/org.eclipse.wst.common.component
@@ -8,6 +8,6 @@
             <dependency-type>uses</dependency-type>
         </dependent-module>
         <property name="java-output-path" value="/vipra-rest/target/classes"/>
-        <property name="context-root" value="rest"/>
+        <property name="context-root" value="vipra-rest"/>
     </wb-module>
 </project-modules>
diff --git a/vipra-rest/pom.xml b/vipra-rest/pom.xml
index bb719431..97251852 100644
--- a/vipra-rest/pom.xml
+++ b/vipra-rest/pom.xml
@@ -20,6 +20,10 @@
 		<log4jVersion>2.4.1</log4jVersion>
 	</properties>
 
+	<build>
+		<finalName>vipra-rest</finalName>
+	</build>
+
 	<dependencies>
 		<!-- Jersey REST -->
 		<dependency>
diff --git a/vipra-rest/src/main/java/de/vipra/rest/model/Article.java b/vipra-rest/src/main/java/de/vipra/rest/model/Article.java
index 15e9003e..c14c71a6 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/model/Article.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/model/Article.java
@@ -4,7 +4,7 @@ import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 
-public class Article extends de.vipra.util.model.Article {
+public class Article extends de.vipra.util.model.Article implements Linked {
 
 	private Map<String, String> links;
 
diff --git a/vipra-rest/src/main/java/de/vipra/rest/model/Linked.java b/vipra-rest/src/main/java/de/vipra/rest/model/Linked.java
index b9f157ba..b3033930 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/model/Linked.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/model/Linked.java
@@ -3,9 +3,7 @@ package de.vipra.rest.model;
 import java.net.URI;
 import java.util.Map;
 
-import de.vipra.util.model.Model;
-
-public abstract class Linked extends Model {
+public interface Linked {
 
 	public abstract Map<String, String> getLinks();
 
diff --git a/vipra-rest/src/main/java/de/vipra/rest/model/Topic.java b/vipra-rest/src/main/java/de/vipra/rest/model/TopicDefinition.java
similarity index 85%
rename from vipra-rest/src/main/java/de/vipra/rest/model/Topic.java
rename to vipra-rest/src/main/java/de/vipra/rest/model/TopicDefinition.java
index d99d4273..a514a4cd 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/model/Topic.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/model/TopicDefinition.java
@@ -4,7 +4,7 @@ import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 
-public class Topic extends de.vipra.util.model.TopicDefinition {
+public class TopicDefinition extends de.vipra.util.model.TopicDefinition implements Linked {
 
 	private Map<String, String> links;
 
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java
index 68626951..edd78cc9 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java
@@ -1,7 +1,7 @@
 package de.vipra.rest.resource;
 
 import java.io.IOException;
-import java.util.ArrayList;
+import java.util.List;
 
 import javax.servlet.ServletContext;
 import javax.ws.rs.Consumes;
@@ -48,8 +48,8 @@ public class ArticleResource {
 	public Response getArticles(@QueryParam("skip") @DefaultValue("0") int skip,
 			@QueryParam("limit") @DefaultValue("0") int limit,
 			@QueryParam("sort") @DefaultValue("date") String sortBy) {
-		ArrayList<Article> articles = service.getArticles(uri.getAbsolutePath(), skip, limit, sortBy);
-		ResponseWrapper<ArrayList<Article>> res = new ResponseWrapper<>(articles);
+		List<Article> articles = service.getMultiple(uri.getAbsolutePath(), skip, limit, sortBy);
+		ResponseWrapper<List<Article>> res = new ResponseWrapper<>(articles);
 		res.addLink("self", uri.getAbsolutePath().toString());
 		return Response.ok().entity(res).build();
 	}
@@ -65,7 +65,7 @@ public class ArticleResource {
 					String.format(Messages.BAD_REQUEST, "id cannot be empty")));
 			return Response.status(Response.Status.BAD_REQUEST).entity(res).build();
 		}
-		Article article = service.getArticle(uri.getAbsolutePath(), id);
+		Article article = service.getSingle(uri.getAbsolutePath(), id);
 		if (article != null) {
 			res.setData(article);
 			return Response.ok().entity(res).build();
@@ -82,7 +82,7 @@ public class ArticleResource {
 	public Response createArticle(Article article) {
 		ResponseWrapper<Article> res;
 		try {
-			article = service.createArticle(uri.getAbsolutePath(), article);
+			article = service.createSingle(uri.getAbsolutePath(), article);
 			res = new ResponseWrapper<>(article);
 			return Response.created(article.uri(uri.getAbsolutePath())).entity(res).build();
 		} catch (DatabaseException e) {
@@ -98,7 +98,7 @@ public class ArticleResource {
 		ResponseWrapper<Article> res = new ResponseWrapper<>();
 		long deleted;
 		try {
-			deleted = service.deleteArticle(id);
+			deleted = service.deleteSingle(id);
 		} catch (DatabaseException e) {
 			res = new ResponseWrapper<>(new APIError(Response.Status.INTERNAL_SERVER_ERROR, "item could not be deleted",
 					"item could not be created due to an internal server error"));
@@ -124,7 +124,7 @@ public class ArticleResource {
 	public Response updateArticle(@PathParam("id") String id, Article article) {
 		ResponseWrapper<Article> res = new ResponseWrapper<>();
 		try {
-			long updated = service.updateArticle(uri.getAbsolutePath(), article);
+			long updated = service.updateSingle(uri.getAbsolutePath(), article);
 			int updatedInt = updated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) updated;
 			switch (updatedInt) {
 			case 0:
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java
index f0b5d571..f2f94cb0 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java
@@ -1,7 +1,7 @@
 package de.vipra.rest.resource;
 
 import java.io.IOException;
-import java.util.ArrayList;
+import java.util.List;
 
 import javax.servlet.ServletContext;
 import javax.ws.rs.Consumes;
@@ -20,12 +20,12 @@ import de.vipra.rest.APIMediaType;
 import de.vipra.rest.Messages;
 import de.vipra.rest.model.APIError;
 import de.vipra.rest.model.ResponseWrapper;
-import de.vipra.rest.model.Topic;
+import de.vipra.rest.model.TopicDefinition;
+import de.vipra.rest.service.TopicService;
 import de.vipra.util.Config;
-import de.vipra.util.Constants;
+import de.vipra.util.Mongo;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.ex.DatabaseException;
-import de.vipra.util.service.DatabaseService;
 
 @Path("topics")
 public class TopicResource {
@@ -33,19 +33,20 @@ public class TopicResource {
 	@Context
 	UriInfo uri;
 
-	DatabaseService<Topic> service;
+	TopicService service;
 
 	public TopicResource(@Context ServletContext servletContext) throws ConfigException, IOException {
 		Config config = Config.getConfig();
-		service = DatabaseService.getDatabaseService(config, Constants.Collection.TOPICS, Topic.class);
+		Mongo mongo = Mongo.getInstance(config);
+		service = new TopicService(mongo);
 	}
 
 	@GET
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	public Response getTopics(@QueryParam("skip") @DefaultValue("0") int skip,
 			@QueryParam("limit") @DefaultValue("0") int limit) {
-		ArrayList<Topic> topics = service.getMultiple(skip, limit, null);
-		ResponseWrapper<ArrayList<Topic>> res = new ResponseWrapper<>(topics);
+		List<TopicDefinition> topics = service.getMultiple(uri.getAbsolutePath(), skip, limit, null);
+		ResponseWrapper<List<TopicDefinition>> res = new ResponseWrapper<>(topics);
 		res.addLink("self", uri.getAbsolutePath().toString());
 		return Response.ok().entity(res).build();
 	}
@@ -55,15 +56,14 @@ public class TopicResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
 	public Response getTopic(@PathParam("id") String id) {
-		ResponseWrapper<Topic> res = new ResponseWrapper<>();
+		ResponseWrapper<TopicDefinition> res = new ResponseWrapper<>();
 		if (id == null || id.trim().length() == 0) {
 			res.addError(new APIError(Response.Status.BAD_REQUEST, "ID is empty",
 					String.format(Messages.BAD_REQUEST, "id cannot be empty")));
 			return Response.status(Response.Status.BAD_REQUEST).entity(res).build();
 		}
-		Topic topic = service.getSingle(id);
+		TopicDefinition topic = service.getSingle(uri.getAbsolutePath(), id);
 		if (topic != null) {
-			topic.setBase(uri.getAbsolutePath());
 			res.setData(topic);
 			return Response.ok().entity(res).build();
 		} else {
@@ -77,8 +77,8 @@ public class TopicResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
-	public Response updateTopic(@PathParam("id") String id, Topic topic) {
-		ResponseWrapper<Topic> res = new ResponseWrapper<>();
+	public Response updateTopic(@PathParam("id") String id, TopicDefinition topic) {
+		ResponseWrapper<TopicDefinition> res = new ResponseWrapper<>();
 		try {
 			long updated = service.updateSingle(topic);
 			int updatedInt = updated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) updated;
diff --git a/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java b/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java
index 139413e1..099194ea 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java
@@ -1,55 +1,26 @@
 package de.vipra.rest.service;
 
 import java.net.URI;
-import java.util.ArrayList;
+import java.util.List;
 
 import de.vipra.rest.model.Article;
 import de.vipra.util.Constants;
 import de.vipra.util.Mongo;
-import de.vipra.util.ex.DatabaseException;
-import de.vipra.util.service.DatabaseService;
 
-public class ArticleService extends DatabaseService<Article> {
+public class ArticleService extends Service<Article> {
 
 	public ArticleService(Mongo mongo) {
 		super(mongo, Constants.Collection.ARTICLES, Article.class);
 	}
 
-	public Article getArticle(URI base, String id) {
-		Article article = super.getSingle(id);
-		if (article != null) {
-			article.setBase(base);
-		}
-		return article;
-	}
-
-	public ArrayList<Article> getArticles(URI base, int skip, int limit, String sortBy) {
-		ArrayList<Article> articles = super.getMultiple(skip, limit, sortBy);
+	public List<Article> getMultiple(URI base, int skip, int limit, String sortBy) {
+		List<Article> articles = super.getMultiple(skip, limit, sortBy);
 		for (Article article : articles) {
 			// delete data for listing
 			article.setText(null);
 			article.setStats(null);
-			article.setBase(base);
 		}
 		return articles;
 	}
 
-	public Article createArticle(URI base, Article article) throws DatabaseException {
-		article = super.createSingle(article);
-		if (article != null) {
-			article.setBase(base);
-		}
-		return article;
-	}
-
-	public long deleteArticle(String id) throws DatabaseException {
-		return super.deleteSingle(id);
-	}
-
-	public long updateArticle(URI base, Article article) throws DatabaseException {
-		long updated = super.updateSingle(article);
-		article.setBase(base);
-		return updated;
-	}
-
 }
diff --git a/vipra-rest/src/main/java/de/vipra/rest/service/Service.java b/vipra-rest/src/main/java/de/vipra/rest/service/Service.java
new file mode 100644
index 00000000..01107f8d
--- /dev/null
+++ b/vipra-rest/src/main/java/de/vipra/rest/service/Service.java
@@ -0,0 +1,53 @@
+package de.vipra.rest.service;
+
+import java.net.URI;
+import java.util.List;
+
+import de.vipra.rest.model.Linked;
+import de.vipra.util.Constants.Collection;
+import de.vipra.util.ex.DatabaseException;
+import de.vipra.util.Mongo;
+import de.vipra.util.model.Model;
+import de.vipra.util.service.DatabaseService;
+
+public class Service<T extends Model & Linked> extends DatabaseService<T> {
+
+	public Service(Mongo mongo, Collection collection, Class<T> clazz) {
+		super(mongo, collection, clazz);
+	}
+
+	public T getSingle(URI base, String id) {
+		T t = super.getSingle(id);
+		if (t != null) {
+			t.setBase(base);
+		}
+		return t;
+	}
+
+	public List<T> getMultiple(URI base, int skip, int limit, String sortBy) {
+		List<T> ts = super.getMultiple(skip, limit, sortBy);
+		for (T t : ts) {
+			t.setBase(base);
+		}
+		return ts;
+	}
+
+	public T createSingle(URI base, T t) throws DatabaseException {
+		t = super.createSingle(t);
+		if (t != null) {
+			t.setBase(base);
+		}
+		return t;
+	}
+
+	public long deleteSingle(String id) throws DatabaseException {
+		return super.deleteSingle(id);
+	}
+
+	public long updateSingle(URI base, T t) throws DatabaseException {
+		long updated = super.updateSingle(t);
+		t.setBase(base);
+		return updated;
+	}
+
+}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java b/vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java
new file mode 100644
index 00000000..56a4897a
--- /dev/null
+++ b/vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java
@@ -0,0 +1,24 @@
+package de.vipra.rest.service;
+
+import java.net.URI;
+import java.util.List;
+
+import de.vipra.rest.model.TopicDefinition;
+import de.vipra.util.Constants;
+import de.vipra.util.Mongo;
+
+public class TopicService extends Service<TopicDefinition> {
+
+	public TopicService(Mongo mongo) {
+		super(mongo, Constants.Collection.TOPICS, TopicDefinition.class);
+	}
+
+	public List<TopicDefinition> getMultiple(URI base, int skip, int limit, String sortBy) {
+		List<TopicDefinition> topics = super.getMultiple(skip, limit, sortBy);
+		for (TopicDefinition topic : topics) {
+			topic.setWords(null);
+		}
+		return topics;
+	}
+
+}
diff --git a/vipra-ui/app/components/dynamic-high-charts.js b/vipra-ui/app/components/dynamic-high-charts.js
index 730778b1..746faf9d 100644
--- a/vipra-ui/app/components/dynamic-high-charts.js
+++ b/vipra-ui/app/components/dynamic-high-charts.js
@@ -1,4 +1,3 @@
-import Ember from 'ember';
 import EmberHighChartsComponent from 'ember-highcharts/components/high-charts';
 
 export default EmberHighChartsComponent.extend({
diff --git a/vipra-ui/app/models/topic.js b/vipra-ui/app/models/topic.js
new file mode 100644
index 00000000..af2db9b4
--- /dev/null
+++ b/vipra-ui/app/models/topic.js
@@ -0,0 +1,5 @@
+import DS from 'ember-data';
+
+export default DS.Model.extend({
+  
+});
diff --git a/vipra-ui/app/router.js b/vipra-ui/app/router.js
index 0a0fb705..8ba6e2d4 100644
--- a/vipra-ui/app/router.js
+++ b/vipra-ui/app/router.js
@@ -10,6 +10,10 @@ Router.map(function() {
     this.route('list', { path: '/' });
     this.route('show', { path: '/:article_id' });
   });
+  this.route('topics', function() {
+  	this.route('list', { path: '/' });
+  	this.route('show', { path: '/:topic_id' });
+  });
   this.route('not-found', { path: '/*:' });
 });
 
diff --git a/vipra-ui/app/routes/topics/list.js b/vipra-ui/app/routes/topics/list.js
new file mode 100644
index 00000000..ef14ad4b
--- /dev/null
+++ b/vipra-ui/app/routes/topics/list.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+  model() {
+    return Ember.RSVP.hash({
+      topics: this.store.findAll('topic')
+    });
+  }
+});
\ No newline at end of file
diff --git a/vipra-ui/app/templates/articles/new.hbs b/vipra-ui/app/templates/articles/new.hbs
deleted file mode 100644
index c1318fdd..00000000
--- a/vipra-ui/app/templates/articles/new.hbs
+++ /dev/null
@@ -1 +0,0 @@
-<h2>New article</h2>
\ No newline at end of file
diff --git a/vipra-ui/app/templates/index.hbs b/vipra-ui/app/templates/index.hbs
index 51b28c7a..3d941a81 100644
--- a/vipra-ui/app/templates/index.hbs
+++ b/vipra-ui/app/templates/index.hbs
@@ -1 +1,5 @@
-<h1>Vipra</h1>
\ No newline at end of file
+<h1>Vipra</h1>
+
+{{#link-to 'articles.list'}}Articles{{/link-to}}
+
+{{#link-to 'topics.list'}}Topics{{/link-to}}
\ No newline at end of file
diff --git a/vipra-ui/app/templates/topics.hbs b/vipra-ui/app/templates/topics.hbs
new file mode 100644
index 00000000..c24cd689
--- /dev/null
+++ b/vipra-ui/app/templates/topics.hbs
@@ -0,0 +1 @@
+{{outlet}}
diff --git a/vipra-ui/app/templates/topics/list.hbs b/vipra-ui/app/templates/topics/list.hbs
new file mode 100644
index 00000000..5db134dd
--- /dev/null
+++ b/vipra-ui/app/templates/topics/list.hbs
@@ -0,0 +1 @@
+<h2>Found topics</h2>
\ No newline at end of file
diff --git a/vipra-ui/app/templates/topics/show.hbs b/vipra-ui/app/templates/topics/show.hbs
new file mode 100644
index 00000000..e69de29b
diff --git a/vipra-ui/tests/unit/models/topic-test.js b/vipra-ui/tests/unit/models/topic-test.js
new file mode 100644
index 00000000..8d71d76e
--- /dev/null
+++ b/vipra-ui/tests/unit/models/topic-test.js
@@ -0,0 +1,12 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('topic', 'Unit | Model | topic', {
+  // Specify the other units that are required for this test.
+  needs: []
+});
+
+test('it exists', function(assert) {
+  let model = this.subject();
+  // let store = this.store();
+  assert.ok(!!model);
+});
diff --git a/vipra-ui/tests/unit/routes/topics-test.js b/vipra-ui/tests/unit/routes/topics-test.js
new file mode 100644
index 00000000..c0218b73
--- /dev/null
+++ b/vipra-ui/tests/unit/routes/topics-test.js
@@ -0,0 +1,11 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('route:topics', 'Unit | Route | topics', {
+  // Specify the other units that are required for this test.
+  // needs: ['controller:foo']
+});
+
+test('it exists', function(assert) {
+  let route = this.subject();
+  assert.ok(route);
+});
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Topic.java b/vipra-util/src/main/java/de/vipra/util/model/Topic.java
deleted file mode 100644
index a693f41e..00000000
--- a/vipra-util/src/main/java/de/vipra/util/model/Topic.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.vipra.util.model;
-
-import java.io.File;
-import java.io.IOException;
-
-import org.bson.Document;
-
-public class Topic extends Model {
-
-	@Override
-	public void fromDocument(Document document) {
-		// TODO Auto-generated method stub
-		
-	}
-
-	@Override
-	public Document toDocument() {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-	@Override
-	public void fromFile(File file) throws IOException {
-		// TODO Auto-generated method stub
-		
-	}
-
-	@Override
-	public String toFileString() {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicDefinition.java b/vipra-util/src/main/java/de/vipra/util/model/TopicDefinition.java
index 921b1755..dbaaecd6 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicDefinition.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicDefinition.java
@@ -12,7 +12,7 @@ import de.vipra.util.ex.NotImplementedException;
 public class TopicDefinition extends Model {
 
 	private int index;
-	private List<String> names;
+	private String name;
 	private List<TopicWord> words;
 
 	public TopicDefinition() {}
@@ -29,12 +29,12 @@ public class TopicDefinition extends Model {
 		this.index = index;
 	}
 
-	public List<String> getNames() {
-		return names;
+	public String getName() {
+		return name;
 	}
 
-	public void setNames(List<String> names) {
-		this.names = names;
+	public void setName(String name) {
+		this.name = name;
 	}
 
 	public List<TopicWord> getWords() {
@@ -48,6 +48,7 @@ public class TopicDefinition extends Model {
 	@SuppressWarnings("unchecked")
 	@Override
 	public void fromDocument(Document document) {
+		setName(document.getString("name"));
 		setIndex(document.getInteger("index", 0));
 		if (document.containsKey("words")) {
 			List<Document> topicWords = (List<Document>) document.get("words");
@@ -61,12 +62,15 @@ public class TopicDefinition extends Model {
 	@Override
 	public Document toDocument() {
 		Document document = new Document();
+		document.append("name", getName());
 		document.append("index", getIndex());
-		List<Document> topicWords = new ArrayList<>(words.size());
-		for (TopicWord word : words) {
-			topicWords.add(word.toDocument());
+		if (getWords() != null) {
+			List<Document> topicWords = new ArrayList<>(words.size());
+			for (TopicWord word : words) {
+				topicWords.add(word.toDocument());
+			}
+			document.put("words", topicWords);
 		}
-		document.put("words", topicWords);
 		return document;
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java b/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java
index b8fe81ed..54adc442 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java
@@ -4,6 +4,7 @@ import static de.vipra.util.MongoUtils.getSorts;
 import static de.vipra.util.MongoUtils.objectId;
 
 import java.util.ArrayList;
+import java.util.List;
 
 import org.bson.Document;
 import org.slf4j.Logger;
@@ -54,10 +55,10 @@ public class DatabaseService<T extends Model> implements Service<T, DatabaseExce
 		}
 	}
 
-	public ArrayList<T> getMultiple(int skip, int limit, String sortBy) {
-		ArrayList<Document> documents = collection.find().skip(skip).limit(limit).sort(getSorts(sortBy))
+	public List<T> getMultiple(int skip, int limit, String sortBy) {
+		List<Document> documents = collection.find().skip(skip).limit(limit).sort(getSorts(sortBy))
 				.into(new ArrayList<Document>());
-		ArrayList<T> items = new ArrayList<>(documents.size());
+		List<T> items = new ArrayList<>(documents.size());
 
 		for (Document document : documents) {
 			items.add(newT(document));
diff --git a/vm/bootstrap.sh b/vm/bootstrap.sh
index bfaa0b1c..78e21dd3 100644
--- a/vm/bootstrap.sh
+++ b/vm/bootstrap.sh
@@ -95,6 +95,7 @@ ufw disable
 # set environment
 
 cat $CONFIG/environment >> /etc/environment
+cat $CONFIG/motd > /etc/motd
 
 # -----------------------------------------------------------------------------
 # cleanup
diff --git a/vm/config/mongod.conf b/vm/config/mongod.conf
deleted file mode 100644
index 6ca9d657..00000000
--- a/vm/config/mongod.conf
+++ /dev/null
@@ -1,41 +0,0 @@
-# mongod.conf
-
-# for documentation of all options, see:
-#   http://docs.mongodb.org/manual/reference/configuration-options/
-
-# Where and how to store data.
-storage:
-  dbPath: /var/lib/mongodb
-  journal:
-    enabled: true
-#  engine:
-#  mmapv1:
-#  wiredTiger:
-
-# where to write logging data.
-systemLog:
-  destination: file
-  logAppend: true
-  path: /var/log/mongodb/mongod.log
-
-# network interfaces
-net:
-  port: 27017
-  bindIp: 0.0.0.0
-
-
-#processManagement:
-
-#security:
-
-#operationProfiling:
-
-#replication:
-
-#sharding:
-
-## Enterprise-Only Options:
-
-#auditLog:
-
-#snmp:
diff --git a/vm/config/motd b/vm/config/motd
new file mode 100644
index 00000000..194c716b
--- /dev/null
+++ b/vm/config/motd
@@ -0,0 +1,8 @@
+====================
+Vipra Development VM
+====================
+
+Start database and web server manually:
+ > service mongod restart
+ > service tomcat restart
+ 
-- 
GitLab