From b4337fa853746256d5804df62370f9425f95d67d Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Mon, 25 Jan 2016 00:38:23 +0100
Subject: [PATCH] updated ui

added imports resource, storing import operation information
added import information to start page
removed war file from repo, updated gitignore
---
 .gitignore                                    |   1 +
 ma-impl.sublime-workspace                     |  98 ++--------
 .../de/vipra/cmd/model/ProcessedArticle.java  |   2 +-
 .../de/vipra/cmd/option/ClearCommand.java     |   4 +
 .../de/vipra/cmd/option/ImportCommand.java    |  60 ++++--
 .../rest/provider/ObjectMapperProvider.java   |  10 +-
 .../vipra/rest/resource/ArticleResource.java  |  36 ++--
 .../vipra/rest/resource/ImportResource.java   | 139 ++++++++++++++
 .../rest/serializer/GenericDeserializer.java  |   2 +
 .../rest/serializer/GenericSerializer.java    |   2 +
 vipra-ui/app/adapters/application.js          |   2 +-
 vipra-ui/app/helpers/is-empty.js              |  11 ++
 vipra-ui/app/models/import.js                 |  12 ++
 vipra-ui/app/router.js                        |   3 +
 vipra-ui/app/routes/index.js                  |  23 +++
 vipra-ui/app/styles/app.scss                  |  21 +++
 vipra-ui/app/templates/application.hbs        |   2 +-
 vipra-ui/app/templates/index.hbs              |  32 +++-
 vipra-ui/tests/unit/helpers/is-empty-test.js  |  10 +
 vipra-ui/tests/unit/models/import-test.js     |  12 ++
 .../main/java/de/vipra/util/StringUtils.java  |  29 +++
 .../src/main/java/de/vipra/util/Timer.java    |   8 +-
 .../src/main/java/de/vipra/util/WordMap.java  |   9 +-
 .../java/de/vipra/util/model/Article.java     | 140 +-------------
 .../java/de/vipra/util/model/ArticleFull.java | 175 ++++++++++++++++++
 .../main/java/de/vipra/util/model/Import.java | 132 +++++++++++++
 .../main/java/de/vipra/util/model/Topic.java  |   6 +
 .../vipra/util/service/DatabaseService.java   |   7 +-
 .../java/de/vipra/util/service/Service.java   |   3 +
 29 files changed, 717 insertions(+), 274 deletions(-)
 create mode 100644 vipra-rest/src/main/java/de/vipra/rest/resource/ImportResource.java
 create mode 100644 vipra-ui/app/helpers/is-empty.js
 create mode 100644 vipra-ui/app/models/import.js
 create mode 100644 vipra-ui/app/routes/index.js
 create mode 100644 vipra-ui/tests/unit/helpers/is-empty-test.js
 create mode 100644 vipra-ui/tests/unit/models/import-test.js
 create mode 100644 vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
 create mode 100644 vipra-util/src/main/java/de/vipra/util/model/Import.java

diff --git a/.gitignore b/.gitignore
index 9878f61c..f19bca38 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 *.log
 *.jar
+*.war
 .vagrant/
 vm/webapps/
diff --git a/ma-impl.sublime-workspace b/ma-impl.sublime-workspace
index 61e4ba59..ba2c4d36 100644
--- a/ma-impl.sublime-workspace
+++ b/ma-impl.sublime-workspace
@@ -279,22 +279,6 @@
 	},
 	"buffers":
 	[
-		{
-			"file": "vipra-ui/app/templates/articles/index.hbs",
-			"settings":
-			{
-				"buffer_size": 385,
-				"line_ending": "Unix"
-			}
-		},
-		{
-			"file": "vipra-ui/app/routes/articles/index.js",
-			"settings":
-			{
-				"buffer_size": 1232,
-				"line_ending": "Unix"
-			}
-		}
 	],
 	"build_system": "",
 	"build_system_choices":
@@ -476,20 +460,24 @@
 		"/home/eike/repos/master/ma-impl",
 		"/home/eike/repos/master/ma-impl/vipra-ui",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/models",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/articles",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/styles",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/components"
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates"
 	],
 	"file_history":
 	[
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles/index.hbs",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/articles/index.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/styles/app.scss",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/index.hbs",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/index.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/models/import.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/application.hbs",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/styles/app.scss",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/helpers/is-empty.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/router.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles/index.hbs",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/articles/index.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/adapters/application.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/styles/sticky-footer.scss",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/controllers/application.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/components/scroll-top.js",
@@ -509,7 +497,6 @@
 		"/home/eike/.config/sublime-text-3/Packages/User/Preferences.sublime-settings",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/words.hbs",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/adapters/application.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/ember-cli-build.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/loading.hbs",
@@ -521,7 +508,6 @@
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/models/word.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/words/index.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/components/items-list.js",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/router.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/words",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/models/topic.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/models/article.js",
@@ -609,10 +595,7 @@
 		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/components/search-box.js",
 		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/templates/loading.hbs",
 		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/templates/not-found.hbs",
-		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/templates/articles.hbs",
-		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/models/article.js",
-		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/templates/index.hbs",
-		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/routes/articles/index.js"
+		"/home/eike/Repositories/fu/ss15/ma/impl/vipra-ui/app/templates/articles.hbs"
 	],
 	"find":
 	{
@@ -936,67 +919,8 @@
 	"groups":
 	[
 		{
-			"selected": 1,
 			"sheets":
 			[
-				{
-					"buffer": 0,
-					"file": "vipra-ui/app/templates/articles/index.hbs",
-					"semi_transient": false,
-					"settings":
-					{
-						"buffer_size": 385,
-						"regions":
-						{
-						},
-						"selection":
-						[
-							[
-								385,
-								385
-							]
-						],
-						"settings":
-						{
-							"syntax": "Packages/Handlebars/grammars/Handlebars.tmLanguage"
-						},
-						"translation.x": 0.0,
-						"translation.y": 0.0,
-						"zoom_level": 1.0
-					},
-					"stack_index": 1,
-					"type": "text"
-				},
-				{
-					"buffer": 1,
-					"file": "vipra-ui/app/routes/articles/index.js",
-					"semi_transient": false,
-					"settings":
-					{
-						"buffer_size": 1232,
-						"regions":
-						{
-						},
-						"selection":
-						[
-							[
-								1212,
-								1212
-							]
-						],
-						"settings":
-						{
-							"syntax": "Packages/JavaScriptNext - ES6 Syntax/JavaScriptNext.tmLanguage",
-							"tab_size": 2,
-							"translate_tabs_to_spaces": true
-						},
-						"translation.x": -0.0,
-						"translation.y": 663.0,
-						"zoom_level": 1.0
-					},
-					"stack_index": 0,
-					"type": "text"
-				}
 			]
 		}
 	],
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java b/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
index aea42f74..484edc6b 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
@@ -8,7 +8,7 @@ import de.vipra.cmd.text.ProcessedText;
 
 @SuppressWarnings("serial")
 @Entity(value = "articles", noClassnameStored = true)
-public class ProcessedArticle extends de.vipra.util.model.Article {
+public class ProcessedArticle extends de.vipra.util.model.ArticleFull {
 
 	@Transient
 	private ProcessedText processedText;
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java
index b56dd596..a71a50a6 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java
@@ -14,6 +14,7 @@ import de.vipra.cmd.model.ProcessedArticle;
 import de.vipra.util.Config;
 import de.vipra.util.ConsoleUtils;
 import de.vipra.util.ex.ConfigException;
+import de.vipra.util.model.Import;
 import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.Word;
 import de.vipra.util.service.DatabaseService;
@@ -28,6 +29,7 @@ public class ClearCommand implements Command {
 	private DatabaseService<ProcessedArticle, ObjectId> dbArticles;
 	private DatabaseService<TopicFull, ObjectId> dbTopics;
 	private DatabaseService<Word, String> dbWords;
+	private DatabaseService<Import, ObjectId> dbImports;
 
 	public ClearCommand(boolean defaults) {
 		this.defaults = defaults;
@@ -39,6 +41,7 @@ public class ClearCommand implements Command {
 			dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class);
 			dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
 			dbWords = DatabaseService.getDatabaseService(config, Word.class);
+			dbImports = DatabaseService.getDatabaseService(config, Import.class);
 		} catch (Exception e) {
 			throw new ClearException(e);
 		}
@@ -47,6 +50,7 @@ public class ClearCommand implements Command {
 		dbArticles.drop();
 		dbTopics.drop();
 		dbWords.drop();
+		dbImports.drop();
 
 		out.info("clearing filebase");
 		File dataDir = config.getDataDirectory();
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
index 7cefa773..7cafd4a3 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
@@ -34,7 +34,10 @@ import de.vipra.util.StringUtils;
 import de.vipra.util.Timer;
 import de.vipra.util.WordMap;
 import de.vipra.util.ex.DatabaseException;
+import de.vipra.util.model.Article;
 import de.vipra.util.model.ArticleStats;
+import de.vipra.util.model.Import;
+import de.vipra.util.model.Topic;
 import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicRef;
 import de.vipra.util.model.Word;
@@ -51,6 +54,7 @@ public class ImportCommand implements Command {
 	private DatabaseService<ProcessedArticle, ObjectId> dbArticles;
 	private DatabaseService<TopicFull, ObjectId> dbTopics;
 	private DatabaseService<Word, String> dbWords;
+	private DatabaseService<Import, ObjectId> dbImports;
 	private Filebase filebase;
 	private Processor preprocessor;
 	private WordMap wordMap;
@@ -103,8 +107,8 @@ public class ImportCommand implements Command {
 	 * @return
 	 * @throws ImportException
 	 */
-	private void importArticle(JSONObject obj) throws ImportException {
-		out.debug("importing \"" + StringUtils.ellipsize(obj.get("title").toString(), 80) + "\"");
+	private Article importArticle(JSONObject obj) throws ImportException {
+		out.info("importing \"" + StringUtils.ellipsize(obj.get("title").toString(), 80) + "\"");
 		ProcessedArticle article = new ProcessedArticle();
 		article.fromJSON(obj);
 
@@ -120,6 +124,9 @@ public class ImportCommand implements Command {
 
 			// add article to filebase
 			filebase.add(article);
+
+			// return article reference
+			return new Article(article.getId());
 		} catch (Exception e) {
 			throw new ImportException(e, article.getId().toString());
 		}
@@ -135,31 +142,29 @@ public class ImportCommand implements Command {
 	 * @throws ImportException
 	 * @throws Exception
 	 */
-	private long importFile(File file) throws FileNotFoundException, IOException, ParseException, ImportException {
+	private List<Article> importFile(File file)
+			throws FileNotFoundException, IOException, ParseException, ImportException {
 		Object data = parser.parse(new FileReader(file));
-
-		long imported = 0;
+		List<Article> articles = new ArrayList<>();
 
 		if (data instanceof JSONArray) {
 			for (Object object : (JSONArray) data) {
-				importArticle((JSONObject) object);
-				imported++;
+				articles.add(importArticle((JSONObject) object));
 			}
 		} else if (data instanceof JSONObject) {
-			importArticle((JSONObject) data);
-			imported++;
+			articles.add(importArticle((JSONObject) data));
 		}
 
-		return imported;
+		return articles;
 	}
 
-	private long importFiles(List<File> files)
+	private List<Article> importFiles(List<File> files)
 			throws FileNotFoundException, IOException, ParseException, ImportException {
-		long imported = 0;
+		List<Article> articles = new ArrayList<>();
 		for (File file : files) {
-			imported += importFile(file);
+			articles.addAll(importFile(file));
 		}
-		return imported;
+		return articles;
 	}
 
 	@Override
@@ -169,6 +174,7 @@ public class ImportCommand implements Command {
 			dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class);
 			dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
 			dbWords = DatabaseService.getDatabaseService(config, Word.class);
+			dbImports = DatabaseService.getDatabaseService(config, Import.class);
 			filebase = Filebase.getFilebase(config);
 			preprocessor = Processor.getPreprocessor(config);
 			wordMap = new WordMap(dbWords);
@@ -181,11 +187,14 @@ public class ImportCommand implements Command {
 			Timer timer = new Timer();
 			timer.start();
 
+			Import importOp = new Import();
+
 			/*
 			 * import files into database and filebase
 			 */
 			out.info("file import");
-			long imported = importFiles(files);
+			List<Article> importedArticles = importFiles(files);
+			importOp.setArticles(importedArticles);
 			timer.lap("import");
 
 			/*
@@ -211,15 +220,19 @@ public class ImportCommand implements Command {
 			Map<String, String> topicIndexMap = new HashMap<>();
 			dbTopics.drop();
 			List<TopicFull> newTopicDefs = new ArrayList<>(batchSize);
+			List<Topic> newTopicRefs = new ArrayList<>();
 			Iterator<TopicFull> it = topicDefs.iterator();
 			while (it.hasNext()) {
 				newTopicDefs.add(it.next());
 				if (newTopicDefs.size() == batchSize || !it.hasNext()) {
 					dbTopics.createMultiple(newTopicDefs);
-					for (TopicFull newTopicDef : newTopicDefs)
+					for (TopicFull newTopicDef : newTopicDefs) {
 						topicIndexMap.put(Integer.toString(newTopicDef.getIndex()), newTopicDef.getId().toString());
+						newTopicRefs.add(new Topic(newTopicDef.getId()));
+					}
 				}
 			}
+			importOp.setTopics(newTopicRefs);
 			timer.lap("saving topics");
 
 			/*
@@ -250,14 +263,23 @@ public class ImportCommand implements Command {
 					log.error("could not update article: " + a.getTitle() + " (" + a.getId() + ")");
 				}
 			}
+			List<Word> importedWords = wordMap.getNewWords();
+			importOp.setWords(importedWords);
 			timer.lap("saving topic refs");
 
+			/*
+			 * save import information
+			 */
+			importOp.setDuration(timer.total());
+			dbImports.createSingle(importOp);
+
 			/*
 			 * run information
 			 */
-			out.info("imported " + imported + " new " + StringUtils.quantity(imported, "article"));
-			long newWords = wordMap.getNewWords();
-			out.info("imported " + newWords + " new " + StringUtils.quantity(newWords, "word"));
+			int newArticlesCount = importedArticles.size();
+			int newWordsCount = importedWords.size();
+			out.info("imported " + newArticlesCount + " new " + StringUtils.quantity(newArticlesCount, "article"));
+			out.info("imported " + newWordsCount + " new " + StringUtils.quantity(newWordsCount, "word"));
 			out.info(timer.toString());
 		} catch (Exception e) {
 			throw new ExecutionException(e);
diff --git a/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java b/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java
index 2b1060cf..fcab9e67 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java
@@ -19,7 +19,8 @@ import de.vipra.rest.serializer.GenericSerializer;
 import de.vipra.rest.serializer.ObjectIdDeserializer;
 import de.vipra.rest.serializer.ObjectIdSerializer;
 import de.vipra.util.Constants;
-import de.vipra.util.model.Article;
+import de.vipra.util.model.ArticleFull;
+import de.vipra.util.model.Import;
 import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.Word;
 
@@ -41,8 +42,8 @@ public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
 
 	public static ObjectMapper createDefaultMapper() {
 		SimpleModule module = new SimpleModule();
-		module.addSerializer(Article.class, new GenericSerializer<Article>(Article.class));
-		module.addDeserializer(Article.class, new GenericDeserializer<Article>(Article.class));
+		module.addSerializer(ArticleFull.class, new GenericSerializer<ArticleFull>(ArticleFull.class));
+		module.addDeserializer(ArticleFull.class, new GenericDeserializer<ArticleFull>(ArticleFull.class));
 
 		module.addSerializer(TopicFull.class, new GenericSerializer<TopicFull>(TopicFull.class));
 		module.addDeserializer(TopicFull.class, new GenericDeserializer<TopicFull>(TopicFull.class));
@@ -50,6 +51,9 @@ public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
 		module.addSerializer(Word.class, new GenericSerializer<Word>(Word.class));
 		module.addDeserializer(Word.class, new GenericDeserializer<Word>(Word.class));
 
+		module.addSerializer(Import.class, new GenericSerializer<Import>(Import.class));
+		module.addDeserializer(Import.class, new GenericDeserializer<Import>(Import.class));
+
 		module.addSerializer(ObjectId.class, new ObjectIdSerializer());
 		module.addDeserializer(ObjectId.class, new ObjectIdDeserializer());
 
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 204b2f1b..3c9e531a 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
@@ -36,7 +36,7 @@ import de.vipra.util.MongoUtils;
 import de.vipra.util.StringUtils;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.ex.DatabaseException;
-import de.vipra.util.model.Article;
+import de.vipra.util.model.ArticleFull;
 import de.vipra.util.service.DatabaseService;
 
 @Path("articles")
@@ -45,18 +45,18 @@ public class ArticleResource {
 	@Context
 	UriInfo uri;
 
-	final Cache<String, Article> cache;
-	final DatabaseService<Article, ObjectId> service;
+	final Cache<String, ArticleFull> cache;
+	final DatabaseService<ArticleFull, ObjectId> service;
 
 	public ArticleResource(@Context ServletContext servletContext) throws ConfigException, IOException {
 		Config config = Config.getConfig();
-		service = DatabaseService.getDatabaseService(config, Article.class);
+		service = DatabaseService.getDatabaseService(config, ArticleFull.class);
 
 		CacheManager manager = (CacheManager) servletContext.getAttribute("cachemanager");
-		Cache<String, Article> articleCache = manager.getCache("articlecache", String.class, Article.class);
+		Cache<String, ArticleFull> articleCache = manager.getCache("articlecache", String.class, ArticleFull.class);
 		if (articleCache == null)
 			articleCache = manager.createCache("articlecache",
-					CacheConfigurationBuilder.newCacheConfigurationBuilder().buildConfig(String.class, Article.class));
+					CacheConfigurationBuilder.newCacheConfigurationBuilder().buildConfig(String.class, ArticleFull.class));
 		this.cache = articleCache;
 	}
 
@@ -64,7 +64,7 @@ public class ArticleResource {
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	public Response getArticles(@QueryParam("skip") Integer skip, @QueryParam("limit") Integer limit,
 			@QueryParam("sort") @DefaultValue("date") String sortBy, @QueryParam("fields") String fields) {
-		Wrapper<List<Article>> res = new Wrapper<>();
+		Wrapper<List<ArticleFull>> res = new Wrapper<>();
 
 		if (skip != null && limit != null)
 			res.addPaginationLinks(uri.getAbsolutePath(), skip, limit, service.count());
@@ -73,7 +73,7 @@ public class ArticleResource {
 			return res.badRequest();
 
 		try {
-			List<Article> articles = service.getMultiple(skip, limit, sortBy, StringUtils.getFields(fields));
+			List<ArticleFull> articles = service.getMultiple(skip, limit, sortBy, StringUtils.getFields(fields));
 			
 			if((skip != null && skip > 0) || (limit != null && limit > 0))
 				res.addMeta("total", service.count());
@@ -92,14 +92,14 @@ public class ArticleResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
 	public Response getArticle(@PathParam("id") String id, @QueryParam("fields") String fields) {
-		Wrapper<Article> res = new Wrapper<>();
+		Wrapper<ArticleFull> res = new Wrapper<>();
 		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 res.badRequest();
 		}
 
-		Article article;
+		ArticleFull article;
 		try {
 			article = getSingle(id, StringUtils.getFields(fields));
 		} catch (Exception e) {
@@ -119,8 +119,8 @@ public class ArticleResource {
 	@POST
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
-	public Response createArticle(Article article) {
-		Wrapper<Article> res;
+	public Response createArticle(ArticleFull article) {
+		Wrapper<ArticleFull> res;
 		try {
 			article = service.createSingle(article);
 			res = new Wrapper<>(article);
@@ -136,7 +136,7 @@ public class ArticleResource {
 	@DELETE
 	@Path("{id}")
 	public Response deleteArticle(@PathParam("id") String id) {
-		Wrapper<Article> res = new Wrapper<>();
+		Wrapper<ArticleFull> res = new Wrapper<>();
 		long deleted;
 		try {
 			deleted = service.deleteSingle(MongoUtils.objectId(id));
@@ -163,9 +163,9 @@ public class ArticleResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
-	public Response replaceArticle(@PathParam("id") String id, Wrapper<Article> wrapper) {
-		Article article = wrapper.getData();
-		Wrapper<Article> res = new Wrapper<>();
+	public Response replaceArticle(@PathParam("id") String id, Wrapper<ArticleFull> wrapper) {
+		ArticleFull article = wrapper.getData();
+		Wrapper<ArticleFull> res = new Wrapper<>();
 		try {
 			service.updateSingle(article);
 			cache.put(id, article);
@@ -177,9 +177,9 @@ public class ArticleResource {
 		}
 	}
 
-	private Article getSingle(String id, String[] fields) {
+	private ArticleFull getSingle(String id, String[] fields) {
 		if (fields == null || fields.length == 0) {
-			Article article = cache.get(id);
+			ArticleFull article = cache.get(id);
 			if (article == null) {
 				article = service.getSingle(MongoUtils.objectId(id));
 				if (article != null)
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/ImportResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/ImportResource.java
new file mode 100644
index 00000000..f4ba2086
--- /dev/null
+++ b/vipra-rest/src/main/java/de/vipra/rest/resource/ImportResource.java
@@ -0,0 +1,139 @@
+package de.vipra.rest.resource;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.bson.types.ObjectId;
+import org.ehcache.Cache;
+import org.ehcache.CacheManager;
+import org.ehcache.config.CacheConfigurationBuilder;
+
+import de.vipra.rest.APIMediaType;
+import de.vipra.rest.Messages;
+import de.vipra.rest.model.APIError;
+import de.vipra.rest.model.Wrapper;
+import de.vipra.util.Config;
+import de.vipra.util.MongoUtils;
+import de.vipra.util.StringUtils;
+import de.vipra.util.ex.ConfigException;
+import de.vipra.util.model.Import;
+import de.vipra.util.service.DatabaseService;
+
+@Path("imports")
+public class ImportResource {
+
+	@Context
+	UriInfo uri;
+
+	final Cache<String, Import> cache;
+	final DatabaseService<Import, ObjectId> service;
+
+	public ImportResource(@Context ServletContext servletContext) throws ConfigException, IOException {
+		Config config = Config.getConfig();
+		service = DatabaseService.getDatabaseService(config, Import.class);
+
+		CacheManager manager = (CacheManager) servletContext.getAttribute("cachemanager");
+		Cache<String, Import> importCache = manager.getCache("importcache", String.class, Import.class);
+		if (importCache == null)
+			importCache = manager.createCache("importcache",
+					CacheConfigurationBuilder.newCacheConfigurationBuilder().buildConfig(String.class, Import.class));
+		this.cache = importCache;
+	}
+
+	@GET
+	@Produces(APIMediaType.APPLICATION_JSONAPI)
+	public Response getImports(@QueryParam("skip") Integer skip, @QueryParam("limit") Integer limit,
+			@QueryParam("sort") @DefaultValue("date") String sortBy, @QueryParam("fields") String fields) {
+		Wrapper<List<Import>> res = new Wrapper<>();
+
+		if (skip != null && limit != null)
+			res.addPaginationLinks(uri.getAbsolutePath(), skip, limit, service.count());
+
+		if (res.hasErrors())
+			return Response.status(Response.Status.BAD_REQUEST).entity(res).build();
+
+		try {
+			List<Import> imports = service.getMultiple(skip, limit, sortBy, false, StringUtils.getFields(fields));
+
+			if ((skip != null && skip > 0) || (limit != null && limit > 0))
+				res.addMeta("total", service.count());
+			else
+				res.addMeta("total", imports.size());
+
+			return res.ok(imports);
+		} catch (Exception e) {
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
+			return Response.status(Response.Status.BAD_REQUEST).entity(res).build();
+		}
+	}
+
+	@GET
+	@Produces(APIMediaType.APPLICATION_JSONAPI)
+	@Consumes(APIMediaType.APPLICATION_JSONAPI)
+	@Path("latest")
+	public Response getLatestImport(@QueryParam("fields") String fields) {
+		Wrapper<Import> res = new Wrapper<>();
+		List<Import> latestImport = service.getMultiple(0, 1, "date", StringUtils.getFields(fields));
+
+		if (latestImport == null || latestImport.size() != 1) {
+			return res.noContent();
+		} else {
+			return res.ok(latestImport.get(0));
+		}
+	}
+
+	@GET
+	@Produces(APIMediaType.APPLICATION_JSONAPI)
+	@Consumes(APIMediaType.APPLICATION_JSONAPI)
+	@Path("{id}")
+	public Response getImport(@PathParam("id") String id, @QueryParam("fields") String fields) {
+		Wrapper<Import> res = new Wrapper<>();
+		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 res.badRequest();
+		}
+
+		Import importOp;
+		try {
+			importOp = getSingle(id, StringUtils.getFields(fields));
+		} catch (Exception e) {
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
+			return res.badRequest();
+		}
+
+		if (importOp != null)
+			return res.ok(importOp);
+		else {
+			res.addError(new APIError(Response.Status.NOT_FOUND, "Resource not found",
+					String.format(Messages.NOT_FOUND, "import", id)));
+			return res.notFound();
+		}
+	}
+
+	private Import getSingle(String id, String[] fields) {
+		if (fields == null || fields.length == 0) {
+			Import importOp = cache.get(id);
+			if (importOp == null) {
+				importOp = service.getSingle(MongoUtils.objectId(id));
+				if (importOp != null)
+					cache.put(id, importOp);
+			}
+			return importOp;
+		} else
+			return service.getSingle(MongoUtils.objectId(id), fields);
+	}
+
+}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java
index 9566837f..d7842dd3 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java
@@ -17,6 +17,7 @@ import com.fasterxml.jackson.core.JsonToken;
 import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
 
+import de.vipra.util.StringUtils;
 import de.vipra.util.an.JsonWrap;
 import de.vipra.util.model.Model;
 
@@ -49,6 +50,7 @@ public class GenericDeserializer<T extends Model<?>> extends JsonDeserializer<T>
 				if (jw != null)
 					name = jw.value() + "." + name;
 
+				name = StringUtils.camelToDashCase(name);
 				allFields.put(name, field);
 
 				String[] parts = name.split("\\.");
diff --git a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java
index d58ca461..801c3d89 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java
@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.SerializerProvider;
 
 import de.vipra.util.NestedMap;
+import de.vipra.util.StringUtils;
 import de.vipra.util.an.JsonType;
 import de.vipra.util.an.JsonWrap;
 import de.vipra.util.model.Model;
@@ -57,6 +58,7 @@ public class GenericSerializer<T extends Model<?>> extends JsonSerializer<T> {
 				if (jw != null)
 					name = jw.value() + "." + name;
 
+				name = StringUtils.camelToDashCase(name);
 				foundFields.put(name, field);
 			}
 		}
diff --git a/vipra-ui/app/adapters/application.js b/vipra-ui/app/adapters/application.js
index c14dc91b..0d442b61 100644
--- a/vipra-ui/app/adapters/application.js
+++ b/vipra-ui/app/adapters/application.js
@@ -1,7 +1,7 @@
 import DS from 'ember-data';
 
 export default DS.JSONAPIAdapter.extend({
-  host: `http://${window.location.hostname}:8000`,
+  host: `http://${window.location.hostname}:8080`,
   namespace: 'vipra-rest',
   updateRecord(store, type, snapshot) {
     var data = {};
diff --git a/vipra-ui/app/helpers/is-empty.js b/vipra-ui/app/helpers/is-empty.js
new file mode 100644
index 00000000..624b9ccb
--- /dev/null
+++ b/vipra-ui/app/helpers/is-empty.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export function isEmpty(params/*, hash*/) {
+  let ary = params[0],
+      text = params[1];
+  if(!ary || typeof ary.length === "undefined" || !ary.length)
+    return text;
+  return null;
+}
+
+export default Ember.Helper.helper(isEmpty);
diff --git a/vipra-ui/app/models/import.js b/vipra-ui/app/models/import.js
new file mode 100644
index 00000000..f7cbb7b5
--- /dev/null
+++ b/vipra-ui/app/models/import.js
@@ -0,0 +1,12 @@
+import DS from 'ember-data';
+
+export default DS.Model.extend({
+  date: DS.attr('date'),
+  duration: DS.attr(),
+  articles: DS.attr(),
+  articlesCount: DS.attr(),
+  topics: DS.attr(),
+  topicsCount: DS.attr(),
+  words: DS.attr(),
+  wordsCount: DS.attr()
+});
diff --git a/vipra-ui/app/router.js b/vipra-ui/app/router.js
index 1d31b063..c4a21fcf 100644
--- a/vipra-ui/app/router.js
+++ b/vipra-ui/app/router.js
@@ -9,14 +9,17 @@ Router.map(function() {
   this.route('articles', function() {
     this.route('show', { path: '/:article_id' });
   });
+
   this.route('topics', function() {
   	this.route('show', { path: '/:topic_id' }, function() {
       this.route('edit');
     });
   });
+
   this.route('words', function() {
     this.route('show', { path: '/:word_id' });
   });
+  
   this.route('not-found', { path: '/*:' });
 });
 
diff --git a/vipra-ui/app/routes/index.js b/vipra-ui/app/routes/index.js
new file mode 100644
index 00000000..e928d1e7
--- /dev/null
+++ b/vipra-ui/app/routes/index.js
@@ -0,0 +1,23 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+  model() {
+    return Ember.RSVP.hash({
+      imports: this.store.findAll('import', {
+        skip: 0,
+        limit: 5
+      }),
+
+      latestimport: this.store.find('import', 'latest')
+    });
+  },
+
+  afterModel(model) {
+    let articles = model.latestimport.get('articles'),
+        topics = model.latestimport.get('topics'),
+        words = model.latestimport.get('words');
+    model.latestarticles = articles.slice(Math.max(articles.length - 5, 0));
+    model.latesttopics = topics.slice(Math.max(topics.length - 5, 0));
+    model.latestwords = words.slice(Math.max(words.length - 5, 0));
+  }
+});
\ No newline at end of file
diff --git a/vipra-ui/app/styles/app.scss b/vipra-ui/app/styles/app.scss
index 0c0ea97f..e072b7b5 100644
--- a/vipra-ui/app/styles/app.scss
+++ b/vipra-ui/app/styles/app.scss
@@ -1,3 +1,12 @@
+@mixin noselect {
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  cursor: default;
+}
+
 body {
   // for navbar
   padding-top: 60px;
@@ -22,6 +31,18 @@ body {
   height: 120px;
 }
 
+.heading {
+  @include noselect;
+  font-size: 82px;
+  margin: 30px 0;
+}
+
+.ellipsize {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 .navbar-default {
   .navbar-nav {
     &> .active {
diff --git a/vipra-ui/app/templates/application.hbs b/vipra-ui/app/templates/application.hbs
index 16d58172..17ab63e8 100644
--- a/vipra-ui/app/templates/application.hbs
+++ b/vipra-ui/app/templates/application.hbs
@@ -8,7 +8,7 @@
         <span class="icon-bar"></span>
         <span class="icon-bar"></span>
       </button>
-      {{link-to 'Vipra' 'index' class='navbar-brand'}}
+      {{link-to 'vipra' 'index' class='navbar-brand'}}
     </div>
 
     <!-- Collect the nav links, forms, and other content for toggling -->
diff --git a/vipra-ui/app/templates/index.hbs b/vipra-ui/app/templates/index.hbs
index 95736593..40099f48 100644
--- a/vipra-ui/app/templates/index.hbs
+++ b/vipra-ui/app/templates/index.hbs
@@ -2,15 +2,39 @@
   <div class="row">
     <div class="col-md-12">
       <div class="text-center">
-        <h1>Vipra</h1>
+        <h1 class="heading">'v&#618;pr&#601;</h1>
       </div>
-      <input type="text" class="form-control input-lg" placeholder="Search...">
+
+      <br>
+      {{debounced-input class='form-control input-lg' placeholder='Search...' value=filter debounce='500'}}
     </div>
   </div>
 
+  <br><br>
   <div class="row">
-    <div class="col-md-12">
-      
+    <div class="col-md-4 text-center">
+      <h4>Latest articles</h4>
+      <ul class="list-unstyled">
+        {{#each model.latestarticles as |article|}}
+          <li class="ellipsize">{{#link-to 'articles.show' article.id title=article.title}}{{article.title}}{{/link-to}}</li>
+        {{/each}}
+      </ul>
+    </div>
+    <div class="col-md-4 text-center">
+      <h4>Latest topics</h4>
+      <ul class="list-unstyled">
+        {{#each model.latesttopics as |topic|}}
+          <li class="ellipsize">{{#link-to 'topics.show' topic.id}}{{topic.name}}{{/link-to}}</li>
+        {{/each}}
+      </ul>
+    </div>
+    <div class="col-md-4 text-center">
+      <h4>Latest words</h4>
+      <ul class="list-unstyled">
+        {{#each model.latestwords as |word|}}
+          <li class="ellipsize">{{#link-to 'words.show' word.id}}{{word.id}}{{/link-to}}</li>
+        {{/each}}
+      </ul>
     </div>
   </div>
 
diff --git a/vipra-ui/tests/unit/helpers/is-empty-test.js b/vipra-ui/tests/unit/helpers/is-empty-test.js
new file mode 100644
index 00000000..cc3fb38e
--- /dev/null
+++ b/vipra-ui/tests/unit/helpers/is-empty-test.js
@@ -0,0 +1,10 @@
+import { isEmpty } from '../../../helpers/is-empty';
+import { module, test } from 'qunit';
+
+module('Unit | Helper | is empty');
+
+// Replace this with your real tests.
+test('it works', function(assert) {
+  let result = isEmpty(42);
+  assert.ok(result);
+});
diff --git a/vipra-ui/tests/unit/models/import-test.js b/vipra-ui/tests/unit/models/import-test.js
new file mode 100644
index 00000000..569cba60
--- /dev/null
+++ b/vipra-ui/tests/unit/models/import-test.js
@@ -0,0 +1,12 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('import', 'Unit | Model | import', {
+  // 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-util/src/main/java/de/vipra/util/StringUtils.java b/vipra-util/src/main/java/de/vipra/util/StringUtils.java
index f45f5cc3..e093670a 100644
--- a/vipra-util/src/main/java/de/vipra/util/StringUtils.java
+++ b/vipra-util/src/main/java/de/vipra/util/StringUtils.java
@@ -143,4 +143,33 @@ public class StringUtils {
 		return fields.split(",");
 	}
 
+	public static String capitalize(String in) {
+		if (in == null || in.length() == 0)
+			return in;
+		return in.substring(0, 1).toUpperCase() + in.substring(1);
+	}
+
+	public static String decapitalize(String in) {
+		if (in == null || in.length() == 0)
+			return in;
+		return in.substring(0, 1).toLowerCase() + in.substring(1);
+	}
+
+	public static String camelToDashCase(String in) {
+		if (in == null || in.length() == 0)
+			return in;
+		return in.replaceAll("([A-Z])", "-$1").toLowerCase();
+	}
+
+	public static String dashToCamelCase(String in) {
+		if (in == null || in.length() == 0)
+			return in;
+		StringBuilder sb = new StringBuilder();
+		String[] parts = in.split("-");
+		sb.append(parts[0]);
+		for (int i = 1; i < parts.length; i++)
+			sb.append(StringUtils.capitalize(parts[i]));
+		return sb.toString();
+	}
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/Timer.java b/vipra-util/src/main/java/de/vipra/util/Timer.java
index 1ec369f9..bfaf35b8 100644
--- a/vipra-util/src/main/java/de/vipra/util/Timer.java
+++ b/vipra-util/src/main/java/de/vipra/util/Timer.java
@@ -6,11 +6,12 @@ import java.util.Map.Entry;
 
 public class Timer {
 
+	private long firstStart;
 	private long start;
 	private Map<String, Long> laps;
 
 	public long start() {
-		start = System.nanoTime();
+		firstStart = start = System.nanoTime();
 		laps = new LinkedHashMap<>();
 		return start;
 	}
@@ -31,6 +32,11 @@ public class Timer {
 		return lap;
 	}
 
+	public long total() {
+		return System.nanoTime() - firstStart;
+	}
+
+	@Override
 	public String toString() {
 		String out = null;
 		if (laps != null && laps.size() > 0) {
diff --git a/vipra-util/src/main/java/de/vipra/util/WordMap.java b/vipra-util/src/main/java/de/vipra/util/WordMap.java
index 69be359d..792f034e 100644
--- a/vipra-util/src/main/java/de/vipra/util/WordMap.java
+++ b/vipra-util/src/main/java/de/vipra/util/WordMap.java
@@ -19,12 +19,13 @@ public class WordMap {
 
 	private final DatabaseService<Word, String> dbWords;
 	private final Map<String, Word> wordMap;
+	private final List<Word> newWords;
 	private boolean createNow = true;
-	private long newWords = 0;
 
 	public WordMap(DatabaseService<Word, String> dbWords) {
 		this.dbWords = dbWords;
 		this.wordMap = new HashMap<>();
+		this.newWords = new ArrayList<>();
 		List<Word> words = dbWords.getAll();
 		for (Word word : words)
 			wordMap.put(word.getWord().toLowerCase(), word);
@@ -45,7 +46,7 @@ public class WordMap {
 		if (createNow) {
 			try {
 				dbWords.createSingle(word);
-				newWords++;
+				newWords.add(word);
 			} catch (DatabaseException e) {
 				log.error("could not create word in database", e);
 				throw new RuntimeException(e);
@@ -60,7 +61,7 @@ public class WordMap {
 			if (!e.getValue().isCreated())
 				newWords.add(e.getValue());
 		dbWords.createMultiple(newWords);
-		this.newWords += newWords.size();
+		this.newWords.addAll(newWords);
 	}
 
 	public boolean isCreateNow() {
@@ -71,7 +72,7 @@ public class WordMap {
 		this.createNow = createNow;
 	}
 
-	public long getNewWords() {
+	public List<Word> getNewWords() {
 		return newWords;
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Article.java b/vipra-util/src/main/java/de/vipra/util/model/Article.java
index 131a99a2..e127b6e3 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/Article.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/Article.java
@@ -1,66 +1,30 @@
 package de.vipra.util.model;
 
-import java.io.File;
-import java.io.IOException;
 import java.io.Serializable;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
 
 import org.bson.types.ObjectId;
-import org.mongodb.morphia.annotations.Embedded;
 import org.mongodb.morphia.annotations.Entity;
 import org.mongodb.morphia.annotations.Id;
 import org.mongodb.morphia.annotations.Index;
 import org.mongodb.morphia.annotations.Indexes;
-import org.mongodb.morphia.annotations.PrePersist;
 
-import de.vipra.util.Constants;
-import de.vipra.util.FileUtils;
-import de.vipra.util.MongoUtils;
-import de.vipra.util.StringUtils;
 import de.vipra.util.an.JsonType;
-import de.vipra.util.an.JsonWrap;
-import de.vipra.util.an.QueryIgnore;
 
 @SuppressWarnings("serial")
 @JsonType("article")
 @Entity(value = "articles", noClassnameStored = true)
 @Indexes({ @Index("title"), @Index("date") })
-public class Article extends FileModel<ObjectId> implements Serializable {
+public class Article implements Model<ObjectId>, Serializable {
 
 	@Id
 	private ObjectId id;
-
-	@JsonWrap("attributes")
 	private String title;
 
-	@JsonWrap("attributes")
-	@QueryIgnore(multi = true)
-	private String text;
-
-	@JsonWrap("attributes")
-	private String url;
-
-	@JsonWrap("attributes")
-	private Date date;
-
-	@Embedded
-	@JsonWrap("attributes")
-	@QueryIgnore(multi = true)
-	private List<TopicRef> topics;
+	public Article() {}
 
-	@Embedded
-	@JsonWrap("attributes")
-	@QueryIgnore(multi = true)
-	private ArticleStats stats;
-
-	@JsonWrap("attributes")
-	private Date created;
-
-	@JsonWrap("attributes")
-	private Date modified;
+	public Article(ObjectId id) {
+		this.id = id;
+	}
 
 	@Override
 	public ObjectId getId() {
@@ -72,10 +36,6 @@ public class Article extends FileModel<ObjectId> implements Serializable {
 		this.id = id;
 	}
 
-	public void setId(String id) {
-		this.id = MongoUtils.objectId(id);
-	}
-
 	public String getTitle() {
 		return title;
 	}
@@ -84,92 +44,4 @@ public class Article extends FileModel<ObjectId> implements Serializable {
 		this.title = title;
 	}
 
-	public String getText() {
-		return text;
-	}
-
-	public void setText(String text) {
-		this.text = text;
-	}
-
-	public String getUrl() {
-		return url;
-	}
-
-	public void setUrl(String url) {
-		this.url = url;
-	}
-
-	public Date getDate() {
-		return date;
-	}
-
-	public void setDate(Date date) {
-		this.date = date;
-	}
-
-	public void setDate(String date) {
-		SimpleDateFormat df = new SimpleDateFormat(Constants.DATETIME_FORMAT);
-		try {
-			setDate(df.parse(date));
-		} catch (ParseException e) {}
-	}
-
-	public List<TopicRef> getTopics() {
-		return topics;
-	}
-
-	public void setTopics(List<TopicRef> topics) {
-		this.topics = topics;
-	}
-
-	public ArticleStats getStats() {
-		return stats;
-	}
-
-	public void setStats(ArticleStats stats) {
-		this.stats = stats;
-	}
-
-	public Date getCreated() {
-		return created;
-	}
-
-	public void setCreated(Date created) {
-		this.created = created;
-	}
-
-	public Date getModified() {
-		return modified;
-	}
-
-	public void setModified(Date modified) {
-		this.modified = modified;
-	}
-
-	@Override
-	public void fromFile(File file) throws IOException {
-		List<String> lines = FileUtils.readFile(file);
-		setTitle(lines.get(0));
-		setText(StringUtils.join(lines.subList(1, lines.size())));
-	}
-
-	@Override
-	public String toFileString() {
-		return getTitle() + "\n" + getText();
-	}
-
-	@PrePersist
-	public void prePersist() {
-		this.modified = new Date();
-		if (this.created == null)
-			this.created = modified;
-	}
-
-	@Override
-	public String toString() {
-		return Article.class.getSimpleName() + "[id:" + id + ", title:" + title + ", url:" + url + ", date:" + date
-				+ ", created:" + created + ", modified:" + modified + "]";
-	}
-
-}
\ No newline at end of file
+}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
new file mode 100644
index 00000000..6fe77f9c
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
@@ -0,0 +1,175 @@
+package de.vipra.util.model;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import org.bson.types.ObjectId;
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Id;
+import org.mongodb.morphia.annotations.Index;
+import org.mongodb.morphia.annotations.Indexes;
+import org.mongodb.morphia.annotations.PrePersist;
+
+import de.vipra.util.Constants;
+import de.vipra.util.FileUtils;
+import de.vipra.util.MongoUtils;
+import de.vipra.util.StringUtils;
+import de.vipra.util.an.JsonType;
+import de.vipra.util.an.JsonWrap;
+import de.vipra.util.an.QueryIgnore;
+
+@SuppressWarnings("serial")
+@JsonType("article")
+@Entity(value = "articles", noClassnameStored = true)
+@Indexes({ @Index("title"), @Index("date") })
+public class ArticleFull extends FileModel<ObjectId> implements Serializable {
+
+	@Id
+	private ObjectId id;
+
+	@JsonWrap("attributes")
+	private String title;
+
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	private String text;
+
+	@JsonWrap("attributes")
+	private String url;
+
+	@JsonWrap("attributes")
+	private Date date;
+
+	@Embedded
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	private List<TopicRef> topics;
+
+	@Embedded
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	private ArticleStats stats;
+
+	@JsonWrap("attributes")
+	private Date created;
+
+	@JsonWrap("attributes")
+	private Date modified;
+
+	@Override
+	public ObjectId getId() {
+		return id;
+	}
+
+	@Override
+	public void setId(ObjectId id) {
+		this.id = id;
+	}
+
+	public void setId(String id) {
+		this.id = MongoUtils.objectId(id);
+	}
+
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	public String getText() {
+		return text;
+	}
+
+	public void setText(String text) {
+		this.text = text;
+	}
+
+	public String getUrl() {
+		return url;
+	}
+
+	public void setUrl(String url) {
+		this.url = url;
+	}
+
+	public Date getDate() {
+		return date;
+	}
+
+	public void setDate(Date date) {
+		this.date = date;
+	}
+
+	public void setDate(String date) {
+		SimpleDateFormat df = new SimpleDateFormat(Constants.DATETIME_FORMAT);
+		try {
+			setDate(df.parse(date));
+		} catch (ParseException e) {}
+	}
+
+	public List<TopicRef> getTopics() {
+		return topics;
+	}
+
+	public void setTopics(List<TopicRef> topics) {
+		this.topics = topics;
+	}
+
+	public ArticleStats getStats() {
+		return stats;
+	}
+
+	public void setStats(ArticleStats stats) {
+		this.stats = stats;
+	}
+
+	public Date getCreated() {
+		return created;
+	}
+
+	public void setCreated(Date created) {
+		this.created = created;
+	}
+
+	public Date getModified() {
+		return modified;
+	}
+
+	public void setModified(Date modified) {
+		this.modified = modified;
+	}
+
+	@Override
+	public void fromFile(File file) throws IOException {
+		List<String> lines = FileUtils.readFile(file);
+		setTitle(lines.get(0));
+		setText(StringUtils.join(lines.subList(1, lines.size())));
+	}
+
+	@Override
+	public String toFileString() {
+		return getTitle() + "\n" + getText();
+	}
+
+	@PrePersist
+	public void prePersist() {
+		this.modified = new Date();
+		if (this.created == null)
+			this.created = modified;
+	}
+
+	@Override
+	public String toString() {
+		return ArticleFull.class.getSimpleName() + "[id:" + id + ", title:" + title + ", url:" + url + ", date:" + date
+				+ ", created:" + created + ", modified:" + modified + "]";
+	}
+
+}
\ No newline at end of file
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Import.java b/vipra-util/src/main/java/de/vipra/util/model/Import.java
new file mode 100644
index 00000000..d5da6a81
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/Import.java
@@ -0,0 +1,132 @@
+package de.vipra.util.model;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+import org.bson.types.ObjectId;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Id;
+import org.mongodb.morphia.annotations.Index;
+import org.mongodb.morphia.annotations.Indexes;
+import org.mongodb.morphia.annotations.PrePersist;
+import org.mongodb.morphia.annotations.Reference;
+
+import de.vipra.util.an.JsonType;
+import de.vipra.util.an.JsonWrap;
+import de.vipra.util.an.QueryIgnore;
+
+@SuppressWarnings("serial")
+@JsonType("import")
+@Entity(value = "imports", noClassnameStored = true)
+@Indexes({ @Index("date") })
+public class Import implements Model<ObjectId>, Serializable {
+
+	@Id
+	private ObjectId id;
+
+	@JsonWrap("attributes")
+	private Date date;
+
+	@JsonWrap("attributes")
+	private long duration;
+
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	@Reference(ignoreMissing = true)
+	private List<Article> articles;
+
+	@JsonWrap("attributes")
+	private int articlesCount;
+
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	@Reference(ignoreMissing = true)
+	private List<Topic> topics;
+
+	@JsonWrap("attributes")
+	private int topicsCount;
+
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	@Reference(ignoreMissing = true)
+	private List<Word> words;
+
+	@JsonWrap("attributes")
+	private int wordsCount;
+
+	@Override
+	public ObjectId getId() {
+		return id;
+	}
+
+	@Override
+	public void setId(ObjectId id) {
+		this.id = id;
+	}
+
+	public Date getDate() {
+		return date;
+	}
+
+	public void setDate(Date date) {
+		this.date = date;
+	}
+
+	public long getDuration() {
+		return duration;
+	}
+
+	public void setDuration(long duration) {
+		this.duration = duration;
+	}
+
+	public List<Article> getArticles() {
+		return articles;
+	}
+
+	public void setArticles(List<Article> articles) {
+		this.articles = articles;
+		if (articles != null)
+			articlesCount = articles.size();
+	}
+
+	public int getArticlesCount() {
+		return articlesCount;
+	}
+
+	public List<Topic> getTopics() {
+		return topics;
+	}
+
+	public void setTopics(List<Topic> topics) {
+		this.topics = topics;
+		if (topics != null)
+			topicsCount = topics.size();
+	}
+
+	public int getTopicsCount() {
+		return topicsCount;
+	}
+
+	public List<Word> getWords() {
+		return words;
+	}
+
+	public void setWords(List<Word> words) {
+		this.words = words;
+		if (words != null)
+			wordsCount = words.size();
+	}
+
+	public int getWordsCount() {
+		return wordsCount;
+	}
+
+	@PrePersist
+	private void prePersist() {
+		if (date == null)
+			date = new Date();
+	}
+
+}
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
index 07568128..ca9ac7bb 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/Topic.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/Topic.java
@@ -21,6 +21,12 @@ public class Topic implements Model<ObjectId>, Serializable {
 	private ObjectId id;
 	private String name;
 
+	public Topic() {}
+
+	public Topic(ObjectId id) {
+		this.id = id;
+	}
+
 	@Override
 	public ObjectId getId() {
 		return id;
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 fa4bc349..fd0b6c1a 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
@@ -57,6 +57,11 @@ public class DatabaseService<T extends Model<?>, U> implements Service<T, U, Dat
 
 	@Override
 	public List<T> getMultiple(Integer skip, Integer limit, String sortBy, String... fields) {
+		return getMultiple(skip, limit, sortBy, true, fields);
+	}
+
+	@Override
+	public List<T> getMultiple(Integer skip, Integer limit, String sortBy, boolean defaultIgnore, String... fields) {
 		Query<T> q = datastore.createQuery(clazz);
 		if (skip != null && skip > 0)
 			q.offset(skip);
@@ -66,7 +71,7 @@ public class DatabaseService<T extends Model<?>, U> implements Service<T, U, Dat
 			q.order(sortBy);
 		if (fields != null && fields.length > 0)
 			q.retrievedFields(true, setMinus(fields, ignoredFieldsMulti));
-		else if (ignoredFieldsMulti.length > 0)
+		else if (!defaultIgnore && ignoredFieldsMulti.length > 0)
 			q.retrievedFields(false, ignoredFieldsMulti);
 		List<T> list = q.asList();
 		return list;
diff --git a/vipra-util/src/main/java/de/vipra/util/service/Service.java b/vipra-util/src/main/java/de/vipra/util/service/Service.java
index b38d35af..95f6a6d0 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/Service.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/Service.java
@@ -10,6 +10,9 @@ public interface Service<Type extends Model<?>, IdType, E extends Exception> {
 
 	List<Type> getMultiple(Integer skip, Integer limit, String sortBy, String... fields) throws E;
 
+	List<Type> getMultiple(Integer skip, Integer limit, String sortBy, boolean noDefaultIgnore, String... fields)
+			throws E;
+
 	List<Type> getAll(String... fields) throws E;
 
 	Type createSingle(Type t) throws E;
-- 
GitLab