From 06c3bad948b177ec920bf3ef56f71282e14aae03 Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Thu, 7 Apr 2016 16:30:31 +0200
Subject: [PATCH] updated

---
 .../vipra/rest/resource/ArticleResource.java  | 24 ++++++++-
 .../rest/resource/BugReportResource.java      |  4 +-
 vipra-cmd/runcfg/CMD.launch                   |  2 +-
 .../main/java/de/vipra/cmd/lda/Analyzer.java  | 33 ++++++++++--
 .../de/vipra/cmd/option/ImportCommand.java    |  2 +-
 .../de/vipra/cmd/option/IndexingCommand.java  | 10 ++++
 .../de/vipra/cmd/option/ModelingCommand.java  |  3 +-
 .../app/html/directives/article-link.html     |  6 ++-
 vipra-ui/app/html/index.html                  |  2 +-
 vipra-ui/app/html/topics/articles.html        |  2 +-
 vipra-ui/app/html/topics/show.html            |  1 -
 vipra-ui/app/js/directives.js                 | 21 +++++++-
 .../main/java/de/vipra/util/ConsoleUtils.java | 53 +++++++++---------
 .../main/java/de/vipra/util/StringUtils.java  | 54 ++++++++++++++-----
 .../java/de/vipra/util/model/ArticleFull.java |  6 ++-
 .../java/de/vipra/util/model/BugReport.java   | 16 +++---
 .../java/de/vipra/util/model/TextEntity.java  |  2 +
 .../de/vipra/util/model/TextEntityCount.java  |  6 +--
 .../de/vipra/util/model/TextEntityFull.java   | 11 ++--
 19 files changed, 189 insertions(+), 69 deletions(-)

diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
index 2e647eb4..162eb7f9 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
@@ -22,6 +22,7 @@ import de.vipra.rest.Messages;
 import de.vipra.rest.model.APIError;
 import de.vipra.rest.model.ResponseWrapper;
 import de.vipra.util.Config;
+import de.vipra.util.Constants;
 import de.vipra.util.MongoUtils;
 import de.vipra.util.StringUtils;
 import de.vipra.util.ex.ConfigException;
@@ -56,7 +57,8 @@ public class ArticleResource {
 	@Produces(MediaType.APPLICATION_JSON)
 	public Response getArticles(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip,
 			@QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("date") final String sortBy,
-			@QueryParam("fields") final String fields, @QueryParam("word") final String word, @QueryParam("entity") final String entity) {
+			@QueryParam("fields") final String fields, @QueryParam("word") final String word, @QueryParam("entity") final String entity,
+			@QueryParam("excerpt") final String excerpt) {
 		final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>();
 
 		if (res.hasErrors())
@@ -83,6 +85,15 @@ public class ArticleResource {
 			else
 				res.addHeader("total", articles.size());
 
+			if (excerpt != null) {
+				int length = Constants.EXCERPT_LENGTH;
+				try {
+					length = Integer.parseInt(excerpt);
+				} catch (final NumberFormatException e) {}
+				for (final ArticleFull article : articles)
+					article.setText(article.serializeExcerpt(length));
+			}
+
 			return res.ok(articles);
 		} catch (final Exception e) {
 			e.printStackTrace();
@@ -95,7 +106,8 @@ public class ArticleResource {
 	@Produces(MediaType.APPLICATION_JSON)
 	@Consumes(MediaType.APPLICATION_JSON)
 	@Path("{id}")
-	public Response getArticle(@PathParam("id") final String id, @QueryParam("fields") final String fields) {
+	public Response getArticle(@PathParam("id") final String id, @QueryParam("fields") final String fields,
+			@QueryParam("excerpt") final String excerpt) {
 		final ResponseWrapper<ArticleFull> 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")));
@@ -112,6 +124,14 @@ public class ArticleResource {
 		}
 
 		if (article != null) {
+			if (excerpt != null) {
+				int length = Constants.EXCERPT_LENGTH;
+				try {
+					length = Integer.parseInt(excerpt);
+				} catch (final NumberFormatException e) {}
+				article.setText(article.serializeExcerpt(length));
+			}
+
 			return res.ok(article);
 		} else {
 			res.addError(new APIError(Response.Status.NOT_FOUND, "Resource not found", String.format(Messages.NOT_FOUND, "article", id)));
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/BugReportResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/BugReportResource.java
index ccd6f962..bd5e0bd1 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/BugReportResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/BugReportResource.java
@@ -106,11 +106,11 @@ public class BugReportResource {
 	@POST
 	@Consumes(MediaType.APPLICATION_JSON)
 	public Response createBugReport(final BugReport bugReport) throws URISyntaxException {
-		ResponseWrapper<BugReport> res = new ResponseWrapper<>();
+		final ResponseWrapper<BugReport> res = new ResponseWrapper<>();
 
 		try {
 			dbBugReports.createSingle(bugReport);
-		} catch (DatabaseException e) {
+		} catch (final DatabaseException e) {
 			e.printStackTrace();
 			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
 			return res.badRequest();
diff --git a/vipra-cmd/runcfg/CMD.launch b/vipra-cmd/runcfg/CMD.launch
index d65397a0..234953e3 100644
--- a/vipra-cmd/runcfg/CMD.launch
+++ b/vipra-cmd/runcfg/CMD.launch
@@ -11,7 +11,7 @@
 </listAttribute>
 <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="de.vipra.cmd.Main"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-dcC yearly -AI /home/eike/repos/master/ma-impl/vm/data/data.json"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-S test -M"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="vipra-cmd"/>
 <stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea"/>
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
index 34c7e783..113ee3e8 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
@@ -118,15 +118,32 @@ public class Analyzer {
 			String line;
 			int iteration = 0;
 			final int maxIterationsLength = Integer.toString(modelConfig.getDynamicMaxIterations()).length();
+
+			long lastStarted = System.nanoTime();
+			long lastDuration = 0;
+			double avgDuration = 0;
+			final double smoothingFactor = 0.005;
+
+			printProgress(0, 0, iteration, maxIterationsLength, 0, modelConfig);
+
 			while ((line = in.readLine()) != null) {
 				if (line.contains("EM iter")) {
 					iteration++;
+
+					// calculate progress
 					final double progress = (double) iteration / modelConfig.getDynamicMaxIterations() * 100.0;
 					final int tenthPercent = (int) (progress - progress % 10) / 10;
-					ConsoleUtils.infoNOLF(" [" + StringUtils.repeat("#", tenthPercent) + StringUtils.repeat(" ", 10 - tenthPercent) + "] "
-							+ StringUtils.pad(Integer.toString((int) Math.floor(progress)), 3, true) + "% ("
-							+ StringUtils.pad(Integer.toString(iteration), maxIterationsLength, true) + "/" + modelConfig.getDynamicMinIterations()
-							+ "-" + modelConfig.getDynamicMaxIterations() + ")\r");
+
+					// calculate remaining duration
+					final long currentTime = System.nanoTime();
+					lastDuration = currentTime - lastStarted;
+					lastStarted = currentTime;
+					if (avgDuration == 0)
+						avgDuration = lastDuration;
+					avgDuration = smoothingFactor * lastDuration + (1 - smoothingFactor) * avgDuration;
+					final long remainingDuration = (long) avgDuration * (modelConfig.getDynamicMaxIterations() - iteration);
+
+					printProgress(tenthPercent, progress, iteration, maxIterationsLength, remainingDuration, modelConfig);
 				}
 			}
 
@@ -436,4 +453,12 @@ public class Analyzer {
 		dbTopicModels.replaceSingle(topicModel);
 	}
 
+	private void printProgress(final int tenthPercent, final double progress, final int iteration, final int maxIterationsLength,
+			final long remainingNanos, final TopicModelConfig modelConfig) {
+		ConsoleUtils.infoNOLF(" [" + StringUtils.repeat("#", tenthPercent) + StringUtils.repeat(" ", 10 - tenthPercent) + "] "
+				+ StringUtils.pad(Integer.toString((int) Math.floor(progress)), 3, true) + "% ("
+				+ StringUtils.pad(Integer.toString(iteration), maxIterationsLength, true) + "/" + modelConfig.getDynamicMinIterations() + "-"
+				+ modelConfig.getDynamicMaxIterations() + ")\r");
+	}
+
 }
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 00191346..4391d21c 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
@@ -302,7 +302,7 @@ public class ImportCommand implements Command {
 		/*
 		 * run information
 		 */
-		ConsoleUtils.info("done in " + StringUtils.timeString(timer.total()));
+		ConsoleUtils.info(" done in " + StringUtils.timeString(timer.total()));
 	}
 
 	@Override
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java
index 30a62dcb..e4026ff9 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java
@@ -15,6 +15,8 @@ import de.vipra.util.ConsoleUtils;
 import de.vipra.util.ESClient;
 import de.vipra.util.ESSerializer;
 import de.vipra.util.MongoUtils;
+import de.vipra.util.StringUtils;
+import de.vipra.util.Timer;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.ex.DatabaseException;
 import de.vipra.util.model.ArticleFull;
@@ -36,6 +38,9 @@ public class IndexingCommand implements Command {
 	private void indexForModel(final TopicModelConfig modelConfig) throws ParseException, IOException, ConfigException, DatabaseException {
 		ConsoleUtils.info("indexing for model: " + modelConfig.getName());
 
+		final Timer timer = new Timer();
+		timer.restart();
+
 		final FilebaseIDDateIndex index = new FilebaseIDDateIndex(modelConfig.getModelDir(config.getDataDirectory()));
 		final String indexName = modelConfig.getName() + "-articles";
 
@@ -64,6 +69,11 @@ public class IndexingCommand implements Command {
 		}
 
 		elasticClient.close();
+
+		/*
+		 * run information
+		 */
+		ConsoleUtils.info(" done in " + StringUtils.timeString(timer.total()));
 	}
 
 	@Override
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
index da6979ef..e2d37b4d 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
@@ -26,6 +26,7 @@ public class ModelingCommand implements Command {
 	private void modelForModel(final TopicModelConfig modelConfig)
 			throws AnalyzerException, ConfigException, DatabaseException, ParseException, IOException, InterruptedException {
 		ConsoleUtils.info("generating model: " + modelConfig.getName());
+		ConsoleUtils.flush();
 
 		final Analyzer analyzer = new Analyzer();
 
@@ -41,7 +42,7 @@ public class ModelingCommand implements Command {
 		/*
 		 * run information
 		 */
-		ConsoleUtils.info("done in " + StringUtils.timeString(timer.total()));
+		ConsoleUtils.info(" done in " + StringUtils.timeString(timer.total()));
 	}
 
 	@Override
diff --git a/vipra-ui/app/html/directives/article-link.html b/vipra-ui/app/html/directives/article-link.html
index 7a64f2c7..3f1c87c3 100644
--- a/vipra-ui/app/html/directives/article-link.html
+++ b/vipra-ui/app/html/directives/article-link.html
@@ -3,5 +3,9 @@
 		<span ng-bind="article.title"></span>
 		<ng-transclude/>
 	</a>
-  <span class="badge pull-right" ng-bind="::article.topicsCount" ng-attr-title="{{::article.topicsCount}} topic(s)" ng-if="::showBadge"></span>
+	<div class="pull-right">
+		<i class="fa text-muted pointer" ng-class="{'fa-chevron-down':!excerptShown, 'fa-chevron-up':excerptShown}" ng-click="toggleExcerpt()" ng-if="::showExcerpt"></i>
+  	<span class="badge" ng-bind="::article.topicsCount" ng-attr-title="{{::article.topicsCount}} topic(s)" ng-if="::showBadge"></span>
+  </div>
+  <div ng-bind="excerpt" ng-if="excerptShown"></div>
 </span>
diff --git a/vipra-ui/app/html/index.html b/vipra-ui/app/html/index.html
index cf456ef8..0d67d1f3 100644
--- a/vipra-ui/app/html/index.html
+++ b/vipra-ui/app/html/index.html
@@ -14,7 +14,7 @@
       <h4>Latest articles</h4>
       <ul class="list-unstyled">
         <li class="ellipsis" ng-repeat="article in latestArticles">
-          <article-link article="::article" badge="false" menu="false"/>
+          <article-link article="::article" badge="false" menu="false" excerpt="false"/>
         </li>
       </ul>
     </div>
diff --git a/vipra-ui/app/html/topics/articles.html b/vipra-ui/app/html/topics/articles.html
index 0a3a6b30..edbbd76f 100644
--- a/vipra-ui/app/html/topics/articles.html
+++ b/vipra-ui/app/html/topics/articles.html
@@ -38,7 +38,7 @@
           <tbody>
             <tr ng-repeat="article in articles">
               <td>
-                <a ui-sref="articles.show({id: article.id})" ng-bind="::article.title"></a>
+                <article-link article="::article"/>
               </td>
             </tr>
           </tbody>
diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html
index ceb2a222..2c44e685 100644
--- a/vipra-ui/app/html/topics/show.html
+++ b/vipra-ui/app/html/topics/show.html
@@ -100,7 +100,6 @@
               </div>
               <div class="col-md-10 message-container">
                 <div class="chart area-chart" id="topicWordChart" highcharts="topicWord"></div>
-                <div class="message text-muted" ng-hide="wordsSelected">No words selected</div>
               </div>
             </div>
           </div>
diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js
index 41f83f96..9b9353fa 100644
--- a/vipra-ui/app/js/directives.js
+++ b/vipra-ui/app/js/directives.js
@@ -46,10 +46,11 @@
     };
   }]);
 
-  app.directive('articleLink', [function() {
+  app.directive('articleLink', ['ArticleFactory', function(ArticleFactory) {
     return {
       scope: {
         article: '=',
+        excerpt: '@',
         badge: '@'
       },
       restrict: 'E',
@@ -57,7 +58,25 @@
       transclude: true,
       templateUrl: 'html/directives/article-link.html',
       link: function($scope) {
+        $scope.showExcerpt = $scope.excerpt !== 'false';
         $scope.showBadge = $scope.badge !== 'false';
+        $scope.toggleExcerpt = function() {
+          if(!$scope.excerptShown) {
+            if($scope.excerpt) {
+              $scope.excerptShown = true;
+            } else {
+              ArticleFactory.get({
+                id: $scope.article.id,
+                excerpt: true
+              }, function(data) {
+                $scope.excerpt = data.text;
+                $scope.excerptShown = true;
+              });
+            }
+          } else {
+            $scope.excerptShown = false;
+          }
+        }
       }
     };
   }]);
diff --git a/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java b/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java
index be5a99b0..39537544 100644
--- a/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java
+++ b/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java
@@ -24,33 +24,34 @@ public class ConsoleUtils {
 		silent = s;
 	}
 
-	private static void print(final PrintStream ps, final boolean newLine, final String msg) {
+	private static int print(final PrintStream ps, final boolean newLine, final String msg) {
 		if (!silent) {
-			if (msg != null)
-				lastLineLength = msg.length();
 			if (newLine)
 				ps.println(msg);
 			else
 				ps.print(msg);
+			if (msg != null) {
+				lastLineLength = msg.length();
+				return lastLineLength;
+			}
 		}
+		return 0;
 	}
 
 	private static String label(final String label) {
 		return StringUtils.pad(label, pad);
 	}
 
-	public static void print(final Level level, final String msg) {
+	public static int print(final Level level, final String msg) {
 		switch (level) {
 			case INFO:
-				info(msg);
-				break;
+				return info(msg);
 			case WARN:
-				warn(msg);
-				break;
+				return warn(msg);
 			case ERROR:
-				error(msg);
-				break;
+				return error(msg);
 		}
+		return 0;
 	}
 
 	public static void clearLine() {
@@ -60,32 +61,36 @@ public class ConsoleUtils {
 		}
 	}
 
-	public static void info(final String msg) {
-		print(System.out, true, label("INFO") + " - " + msg);
+	public static void flush() {
+		System.out.flush();
+	}
+
+	public static int info(final String msg) {
+		return print(System.out, true, label("INFO") + " - " + msg);
 	}
 
-	public static void infoNOLF(final String msg) {
-		print(System.out, false, label("INFO") + " - " + msg);
+	public static int infoNOLF(final String msg) {
+		return print(System.out, false, label("INFO") + " - " + msg);
 	}
 
-	public static void warn(final String msg) {
-		print(System.out, true, label("WARN") + " - " + msg);
+	public static int warn(final String msg) {
+		return print(System.out, true, label("WARN") + " - " + msg);
 	}
 
-	public static void warnNOLF(final String msg) {
-		print(System.out, false, label("WARN") + " - " + msg);
+	public static int warnNOLF(final String msg) {
+		return print(System.out, false, label("WARN") + " - " + msg);
 	}
 
-	public static void error(final String msg) {
-		print(System.out, true, label("ERROR") + " - " + Ansi.ansi().fg(Color.RED).a(msg).reset());
+	public static int error(final String msg) {
+		return print(System.out, true, label("ERROR") + " - " + Ansi.ansi().fg(Color.RED).a(msg).reset());
 	}
 
-	public static void errorNOLF(final String msg) {
-		print(System.out, false, label("ERROR") + " - " + Ansi.ansi().fg(Color.RED).a(msg).reset());
+	public static int errorNOLF(final String msg) {
+		return print(System.out, false, label("ERROR") + " - " + Ansi.ansi().fg(Color.RED).a(msg).reset());
 	}
 
-	public static void error(final Throwable t) {
-		error(t.getMessage());
+	public static int error(final Throwable t) {
+		return error(t.getMessage());
 	}
 
 	public static double readDouble(String message, final Double def, final Double min, final Double max, final boolean showBounds) {
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 c054ac0a..d8b6d69b 100644
--- a/vipra-util/src/main/java/de/vipra/util/StringUtils.java
+++ b/vipra-util/src/main/java/de/vipra/util/StringUtils.java
@@ -50,7 +50,7 @@ public class StringUtils {
 		return join(Arrays.asList(arr), separator, reverse);
 	}
 
-	public static String timeString(long nanos) {
+	public static String timeString(long nanos, final boolean showMillis, final boolean compactHMS) {
 		final List<String> parts = new ArrayList<String>(6);
 
 		final long days = TimeUnit.NANOSECONDS.toDays(nanos);
@@ -60,26 +60,44 @@ public class StringUtils {
 		}
 
 		final long hours = TimeUnit.NANOSECONDS.toHours(nanos);
-		if (hours > 0) {
-			parts.add(hours + "h");
+		if (hours > 0 || compactHMS) {
+			if (compactHMS) {
+				parts.add(StringUtils.padNumber(hours, 2));
+				parts.add(":");
+			} else if (hours > 0)
+				parts.add(hours + "h");
 			nanos -= TimeUnit.HOURS.toNanos(hours);
 		}
 
 		final long minutes = TimeUnit.NANOSECONDS.toMinutes(nanos);
-		if (minutes > 0) {
-			parts.add(minutes + "m");
+		if (minutes > 0 || compactHMS) {
+			if (compactHMS) {
+				parts.add(StringUtils.padNumber(minutes, 2));
+				parts.add(":");
+			} else if (minutes > 0)
+				parts.add(minutes + "m");
 			nanos -= TimeUnit.MINUTES.toNanos(minutes);
+
 		}
 
 		final long seconds = TimeUnit.NANOSECONDS.toSeconds(nanos);
-		if (seconds > 0) {
-			parts.add(seconds + "s");
+		if (seconds > 0 || compactHMS) {
+			if (compactHMS)
+				parts.add(StringUtils.padNumber(seconds, 2));
+			else if (seconds > 0)
+				parts.add(seconds + "s");
 			nanos -= TimeUnit.SECONDS.toNanos(seconds);
 		}
 
-		final long millis = TimeUnit.NANOSECONDS.toMillis(nanos);
-		if (millis > 0) {
-			parts.add(millis + "ms");
+		if (showMillis) {
+			final long millis = TimeUnit.NANOSECONDS.toMillis(nanos);
+			if (millis > 0 || compactHMS) {
+				if (compactHMS) {
+					parts.add(".");
+					parts.add(StringUtils.padNumber(millis, 4));
+				} else
+					parts.add(millis + "ms");
+			}
 		}
 
 		if (parts.size() == 0) {
@@ -89,14 +107,26 @@ public class StringUtils {
 		return StringUtils.join(parts);
 	}
 
-	public static String padNumber(final int number, final int length) {
-		String lc = Integer.toString(number);
+	public static String timeString(final long nanos, final boolean showMillis) {
+		return timeString(nanos, showMillis, false);
+	}
+
+	public static String timeString(final long nanos) {
+		return timeString(nanos, true, false);
+	}
+
+	public static String padNumber(final long number, final int length) {
+		String lc = Long.toString(number);
 		while (lc.length() < length) {
 			lc = "0" + lc;
 		}
 		return lc;
 	}
 
+	public static String padNumber(final int number, final int length) {
+		return padNumber((long) number, length);
+	}
+
 	/**
 	 * Turns byte counts into human readable strings. Taken from
 	 * https://stackoverflow.com/questions/3758606
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
index 98a7453f..cebad677 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
@@ -119,7 +119,11 @@ public class ArticleFull implements Model<ObjectId>, Serializable {
 
 	@ElasticIndex("excerpt")
 	public String serializeExcerpt() {
-		return StringUtils.ellipsize(text.replaceAll("<[^>]*>", ""), Constants.EXCERPT_LENGTH);
+		return serializeExcerpt(Constants.EXCERPT_LENGTH);
+	}
+
+	public String serializeExcerpt(final int length) {
+		return StringUtils.ellipsize(text.replaceAll("<[^>]*>", ""), length);
 	}
 
 	@ElasticIndex("text")
diff --git a/vipra-util/src/main/java/de/vipra/util/model/BugReport.java b/vipra-util/src/main/java/de/vipra/util/model/BugReport.java
index 1e04e568..d8d138e1 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/BugReport.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/BugReport.java
@@ -41,7 +41,7 @@ public class BugReport implements Model<ObjectId>, Serializable {
 	}
 
 	@Override
-	public void setId(ObjectId id) {
+	public void setId(final ObjectId id) {
 		this.id = id;
 	}
 
@@ -49,7 +49,7 @@ public class BugReport implements Model<ObjectId>, Serializable {
 		return route;
 	}
 
-	public void setRoute(String route) {
+	public void setRoute(final String route) {
 		this.route = route;
 	}
 
@@ -57,7 +57,7 @@ public class BugReport implements Model<ObjectId>, Serializable {
 		return userAgent;
 	}
 
-	public void setUserAgent(String userAgent) {
+	public void setUserAgent(final String userAgent) {
 		this.userAgent = userAgent;
 	}
 
@@ -65,7 +65,7 @@ public class BugReport implements Model<ObjectId>, Serializable {
 		return description;
 	}
 
-	public void setDescription(String description) {
+	public void setDescription(final String description) {
 		this.description = description;
 	}
 
@@ -73,7 +73,7 @@ public class BugReport implements Model<ObjectId>, Serializable {
 		return reproducible;
 	}
 
-	public void setReproducible(Boolean reproducible) {
+	public void setReproducible(final Boolean reproducible) {
 		this.reproducible = reproducible;
 	}
 
@@ -81,7 +81,7 @@ public class BugReport implements Model<ObjectId>, Serializable {
 		return created;
 	}
 
-	public void setCreated(Date created) {
+	public void setCreated(final Date created) {
 		this.created = created;
 	}
 
@@ -89,13 +89,13 @@ public class BugReport implements Model<ObjectId>, Serializable {
 		return screenshot;
 	}
 
-	public void setScreenshot(String screenshot) {
+	public void setScreenshot(final String screenshot) {
 		this.screenshot = screenshot;
 	}
 
 	@PrePersist
 	private void prePersist() {
-		this.created = new Date();
+		created = new Date();
 	}
 
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java b/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java
index 4c5b88b5..e5423b29 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java
@@ -28,10 +28,12 @@ public class TextEntity implements Model<String>, Serializable {
 		this.url = url;
 	}
 
+	@Override
 	public String getId() {
 		return id;
 	}
 
+	@Override
 	public void setId(final String id) {
 		this.id = id;
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TextEntityCount.java b/vipra-util/src/main/java/de/vipra/util/model/TextEntityCount.java
index 7f16790e..d31d5858 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TextEntityCount.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TextEntityCount.java
@@ -17,7 +17,7 @@ public class TextEntityCount implements Comparable<TextEntityCount>, Serializabl
 		return entity;
 	}
 
-	public void setEntity(TextEntity entity) {
+	public void setEntity(final TextEntity entity) {
 		this.entity = entity;
 	}
 
@@ -25,12 +25,12 @@ public class TextEntityCount implements Comparable<TextEntityCount>, Serializabl
 		return count;
 	}
 
-	public void setCount(Integer count) {
+	public void setCount(final Integer count) {
 		this.count = count;
 	}
 
 	@Override
-	public int compareTo(TextEntityCount o) {
+	public int compareTo(final TextEntityCount o) {
 		return count.compareTo(o.getCount());
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TextEntityFull.java b/vipra-util/src/main/java/de/vipra/util/model/TextEntityFull.java
index 8d20134f..257b3651 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TextEntityFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TextEntityFull.java
@@ -20,7 +20,6 @@ public class TextEntityFull implements Model<String>, Serializable {
 	@QueryIgnore(multi = true)
 	private TopicModel topicModel;
 
-	@QueryIgnore(multi = true)
 	private String url;
 
 	@QueryIgnore(multi = true)
@@ -29,15 +28,17 @@ public class TextEntityFull implements Model<String>, Serializable {
 	public TextEntityFull() {}
 
 	public TextEntityFull(final TextEntity textEntity) {
-		this.id = textEntity.getId();
-		this.url = textEntity.getUrl();
-		this.types = textEntity.getTypes();
+		id = textEntity.getId();
+		url = textEntity.getUrl();
+		types = textEntity.getTypes();
 	}
 
+	@Override
 	public String getId() {
 		return id;
 	}
 
+	@Override
 	public void setId(final String id) {
 		this.id = id;
 	}
@@ -46,7 +47,7 @@ public class TextEntityFull implements Model<String>, Serializable {
 		return topicModel;
 	}
 
-	public void setTopicModel(TopicModel topicModel) {
+	public void setTopicModel(final TopicModel topicModel) {
 		this.topicModel = topicModel;
 	}
 
-- 
GitLab