From 6a2f4b845a3bf8ea6da01b6c445b5b27409b299a Mon Sep 17 00:00:00 2001 From: Eike Cochu <eike@cochu.com> Date: Thu, 5 May 2016 00:37:47 +0200 Subject: [PATCH] updated network and other things --- .../provider/RestServletContextListener.java | 5 + .../vipra/rest/resource/ArticleResource.java | 10 +- .../de/vipra/rest/resource/InfoResource.java | 10 +- .../rest/resource/TextEntityResource.java | 10 +- .../de/vipra/rest/resource/TopicResource.java | 11 +- .../de/vipra/rest/resource/WordResource.java | 10 +- .../src/main/java/de/vipra/ws/WebSocket.java | 34 +- .../main/java/de/vipra/ws/WebSocketPing.java | 34 ++ vipra-cmd/runcfg/CMD.launch | 2 +- .../src/main/java/de/vipra/cmd/Main.java | 11 +- .../main/java/de/vipra/cmd/lda/Analyzer.java | 6 + .../de/vipra/cmd/option/ClearCommand.java | 5 + .../java/de/vipra/cmd/option/Command.java | 2 + .../vipra/cmd/option/CreateModelCommand.java | 5 + .../vipra/cmd/option/DeleteModelCommand.java | 5 + .../de/vipra/cmd/option/EditModelCommand.java | 5 + .../de/vipra/cmd/option/ImportCommand.java | 5 + .../de/vipra/cmd/option/IndexingCommand.java | 5 + .../vipra/cmd/option/ListModelsCommand.java | 5 + .../de/vipra/cmd/option/MessageCommand.java | 5 + .../de/vipra/cmd/option/ModelingCommand.java | 11 + .../vipra/cmd/option/PrintModelCommand.java | 5 + .../java/de/vipra/cmd/option/TestCommand.java | 7 +- vipra-ui/app/html/about.html | 66 +++- vipra-ui/app/html/articles/index.html | 11 + .../app/html/directives/char-selector.html | 29 ++ .../html/directives/sequence-dropdown.html | 3 +- .../app/html/directives/window-dropdown.html | 12 + vipra-ui/app/html/entities/index.html | 11 + vipra-ui/app/html/network.html | 64 ++-- vipra-ui/app/html/topics/index.html | 11 + vipra-ui/app/html/words/index.html | 11 + vipra-ui/app/index.html | 5 +- vipra-ui/app/js/app.js | 42 ++- vipra-ui/app/js/controllers.js | 319 +++++++++++++----- vipra-ui/app/js/directives.js | 45 ++- vipra-ui/app/js/factories.js | 37 +- vipra-ui/app/less/app.less | 73 +++- vipra-ui/bower.json | 1 - vipra-ui/gulpfile.js | 1 - .../src/main/java/de/vipra/util/Config.java | 2 +- .../main/java/de/vipra/util/ConsoleUtils.java | 2 +- .../main/java/de/vipra/util/Constants.java | 5 + .../main/java/de/vipra/util/IPCClient.java | 25 +- .../src/main/java/de/vipra/util/LockFile.java | 8 +- .../de/vipra/util/model/TopicModelConfig.java | 10 + .../de/vipra/util/service/MongoService.java | 2 +- .../de/vipra/util/service/QueryBuilder.java | 104 ++++-- 48 files changed, 916 insertions(+), 191 deletions(-) create mode 100644 vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java create mode 100644 vipra-ui/app/html/directives/char-selector.html create mode 100644 vipra-ui/app/html/directives/window-dropdown.html diff --git a/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java b/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java index 9f932b39..57099323 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java +++ b/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger; import de.vipra.util.IPCServer; import de.vipra.ws.WebSocketChain; +import de.vipra.ws.WebSocketPing; public class RestServletContextListener implements ServletContextListener { @@ -22,6 +23,10 @@ public class RestServletContextListener implements ServletContextListener { ipcServer.register("websocket", new WebSocketChain()); ipcServer.start(); log.info("started ipc server"); + + final WebSocketPing wsPing = WebSocketPing.getInstance(); + wsPing.start(); + log.info("started websocket ping"); } } 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 2fae2a24..b0a9e40d 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 @@ -58,7 +58,7 @@ public class ArticleResource { 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("excerpt") final String excerpt) { + @QueryParam("excerpt") final String excerpt, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains) { final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>(); if (res.hasErrors()) @@ -78,10 +78,16 @@ public class ArticleResource { if (entity != null && !entity.isEmpty()) query.eq("entities.entity.id", entity); + if (startChar != null && !startChar.isEmpty()) + query.startsWith("title", startChar, true); + + if (contains != null && !contains.isEmpty()) + query.contains("title", contains, true); + final List<ArticleFull> articles = dbArticles.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) - res.addHeader("total", dbArticles.count(query.skip(null).limit(null))); + res.addHeader("total", dbArticles.count(query)); else res.addHeader("total", articles.size()); diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java index 7a5c47d7..7048ced4 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java @@ -63,20 +63,27 @@ public class InfoResource { // constants info.put("const.windowres", Constants.WINDOW_RESOLUTION); + info.put("const.procmode", Constants.PROCESSOR_MODE); info.put("const.importbuf", Constants.IMPORT_BUFFER_MAX); info.put("const.esboosttopics", Constants.ES_BOOST_TOPICS); info.put("const.esboosttitles", Constants.ES_BOOST_TITLES); info.put("const.topicautoname", Constants.TOPIC_AUTO_NAMING_WORDS); info.put("const.ktopics", Constants.K_TOPICS); + info.put("const.ktopwords", Constants.K_TOP_WORDS); info.put("const.decaylambda", Constants.RISING_DECAY_LAMBDA); + info.put("const.mintopicshare", Constants.MIN_TOPIC_SHARE); info.put("const.minrelprob", Constants.MIN_RELATIVE_PROB); info.put("const.maxsimdocs", Constants.MAX_SIMILAR_DOCUMENTS); - info.put("const.maxdiv", Constants.MAX_SIMILAR_DOCUMENTS_DIVERGENCE); + info.put("const.maxsimtopics", Constants.MAX_SIMILAR_TOPICS); + info.put("const.maxdocdiv", Constants.MAX_SIMILAR_DOCUMENTS_DIVERGENCE); + info.put("const.maxtopicdiv", Constants.MAX_SIMILAR_TOPICS_DIVERGENCE); info.put("const.dynminiter", Constants.DYNAMIC_MIN_ITER); info.put("const.dynmaxiter", Constants.DYNAMIC_MAX_ITER); info.put("const.statiter", Constants.STATIC_ITER); info.put("const.docminlength", Constants.DOCUMENT_MIN_LENGTH); info.put("const.docminwordfreq", Constants.DOCUMENT_MIN_WORD_FREQ); + info.put("const.spotsupport", Constants.SPOTLIGHT_SUPPORT); + info.put("const.spotconf", Constants.SPOTLIGHT_CONFIDENCE); info.put("const.charsdisallow", Constants.CHARS_DISALLOWED); info.put("const.regexemail", Constants.REGEX_EMAIL); info.put("const.regexurl", Constants.REGEX_URL); @@ -84,6 +91,7 @@ public class InfoResource { info.put("const.regexchar", Constants.REGEX_SINGLECHAR); info.put("const.excerptlength", Constants.EXCERPT_LENGTH); info.put("const.dateformat", Constants.DATETIME_FORMAT); + } catch (final Exception e) { info.put("error", e.getMessage()); } diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java index cbb74e61..da867de6 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java @@ -44,7 +44,7 @@ public class TextEntityResource { @Produces(MediaType.APPLICATION_JSON) public Response getEntities(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("id") final String sortBy, - @QueryParam("fields") final String fields) { + @QueryParam("fields") final String fields, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains) { final ResponseWrapper<List<TextEntityFull>> res = new ResponseWrapper<>(); if (res.hasErrors()) @@ -58,10 +58,16 @@ public class TextEntityResource { if (topicModel != null && !topicModel.isEmpty()) query.eq("topicModel", new TopicModel(topicModel)); + if (startChar != null && !startChar.isEmpty()) + query.startsWith("id", startChar, true); + + if (contains != null && !contains.isEmpty()) + query.contains("id", contains, true); + final List<TextEntityFull> entities = dbEntities.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) - res.addHeader("total", dbEntities.count(null)); + res.addHeader("total", dbEntities.count(query)); else res.addHeader("total", entities.size()); diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java index 0aceea06..bf0eafe7 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java @@ -50,7 +50,8 @@ public class TopicResource { @Produces(MediaType.APPLICATION_JSON) public Response getTopics(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("name") final String sortBy, - @QueryParam("fields") final String fields, @QueryParam("word") final String word) { + @QueryParam("fields") final String fields, @QueryParam("word") final String word, @QueryParam("char") final String startChar, + @QueryParam("contains") final String contains) { final ResponseWrapper<List<TopicFull>> res = new ResponseWrapper<>(); if (res.hasErrors()) @@ -67,10 +68,16 @@ public class TopicResource { if (word != null && !word.isEmpty()) query.eq("words.id", word); + if (startChar != null && !startChar.isEmpty()) + query.startsWith("name", startChar, true); + + if (contains != null && !contains.isEmpty()) + query.contains("name", contains, true); + final List<TopicFull> topics = dbTopics.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) - res.addHeader("total", dbTopics.count(query.skip(null).limit(null))); + res.addHeader("total", dbTopics.count(query)); else res.addHeader("total", topics.size()); diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java index 572e2a8e..4921d0a8 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java @@ -40,7 +40,7 @@ public class WordResource { @Produces(MediaType.APPLICATION_JSON) public Response getWords(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("id") final String sortBy, - @QueryParam("fields") final String fields) { + @QueryParam("fields") final String fields, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains) { final ResponseWrapper<List<WordFull>> res = new ResponseWrapper<>(); if (res.hasErrors()) @@ -54,10 +54,16 @@ public class WordResource { if (topicModel != null && !topicModel.isEmpty()) query.eq("topicModel", new TopicModel(topicModel)); + if (startChar != null && !startChar.isEmpty()) + query.startsWith("id", startChar, true); + + if (contains != null && !contains.isEmpty()) + query.contains("id", contains, true); + final List<WordFull> words = dbWords.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) - res.addHeader("total", dbWords.count(null)); + res.addHeader("total", dbWords.count(query)); else res.addHeader("total", words.size()); diff --git a/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java b/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java index 7e31d080..ab910cca 100644 --- a/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java +++ b/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java @@ -1,6 +1,7 @@ package de.vipra.ws; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.HashSet; import java.util.Set; @@ -51,11 +52,42 @@ public class WebSocket { } public static void sendMessage(final String message) { - for (final Session session : sessions) + for (final Session session : sessions) { if (session.isOpen()) session.getAsyncRemote().sendText(message); else sessions.remove(session); + } + } + + public static void sendPing() { + final String msg = "ping"; + final ByteBuffer data = ByteBuffer.wrap(msg.getBytes()); + for (final Session session : sessions) { + if (session.isOpen()) { + try { + session.getAsyncRemote().sendPing(data); + } catch (IllegalArgumentException | IOException e) { + sessions.remove(session); + } + } else + sessions.remove(session); + } + } + + public static void sendPong() { + final String msg = "pong"; + final ByteBuffer data = ByteBuffer.wrap(msg.getBytes()); + for (final Session session : sessions) { + if (session.isOpen()) { + try { + session.getAsyncRemote().sendPong(data); + } catch (IllegalArgumentException | IOException e) { + sessions.remove(session); + } + } else + sessions.remove(session); + } } } diff --git a/vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java b/vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java new file mode 100644 index 00000000..d02c358b --- /dev/null +++ b/vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java @@ -0,0 +1,34 @@ +package de.vipra.ws; + +public class WebSocketPing extends Thread { + + private static WebSocketPing instance; + + private WebSocketPing() {} + + @Override + public void run() { + while (true) { + try { + sleep(60000); + } catch (final InterruptedException e) { + return; + } + WebSocket.sendPong(); + } + } + + @Override + public void start() { + if (!isAlive()) + super.start(); + } + + public static WebSocketPing getInstance() { + if (instance == null) { + instance = new WebSocketPing(); + } + return instance; + } + +} diff --git a/vipra-cmd/runcfg/CMD.launch b/vipra-cmd/runcfg/CMD.launch index b36db2e1..0e3fecb1 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="-t"/> +<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-Ai"/> <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/Main.java b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java index 8c8d894f..9502c5cd 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/Main.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java @@ -54,7 +54,7 @@ public class Main { } } - private static void execute(final String[] args) { + public static void main(final String[] args) { final CommandLineOptions opts = new CommandLineOptions(); try { opts.parse(args); @@ -113,8 +113,13 @@ public class Main { // run if (commands.size() > 0) { + boolean locked = false; for (final ListIterator<Command> it = commands.listIterator(); it.hasNext();) { final Command c = it.next(); + if (c.requiresLock() && !locked) { + lock(); + locked = true; + } try { c.run(); } catch (final Exception e) { @@ -130,11 +135,7 @@ public class Main { } else { opts.printHelp(); } - } - public static void main(final String[] args) { - lock(); - execute(args); unlock(); } 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 f7fd26ca..50e92ca4 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 @@ -460,6 +460,12 @@ public class Analyzer { } } } + + Collections.sort(similarTopics); + + if (similarTopics.size() > modelConfig.getMaxSimilarTopics()) + similarTopics.subList(modelConfig.getMaxSimilarTopics(), similarTopics.size()).clear(); + topic1.setSimilarTopics(similarTopics); } 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 5cd3ef88..1894e665 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 @@ -41,4 +41,9 @@ public class ClearCommand implements Command { clear(); } + @Override + public boolean requiresLock() { + return true; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java index d794b759..48af4869 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java @@ -4,4 +4,6 @@ public interface Command { public void run() throws Exception; + public boolean requiresLock(); + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java index e0292350..63ece339 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java @@ -64,4 +64,9 @@ public class CreateModelCommand implements Command { } } + @Override + public boolean requiresLock() { + return false; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java index 483c320f..9f28494b 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java @@ -57,4 +57,9 @@ public class DeleteModelCommand implements Command { } } + @Override + public boolean requiresLock() { + return true; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java index 43cfbf84..ce25ce87 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java @@ -73,4 +73,9 @@ public class EditModelCommand implements Command { } } + @Override + public boolean requiresLock() { + return true; + } + } 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 92b2636e..116b67c2 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 @@ -356,4 +356,9 @@ public class ImportCommand implements Command { ipcClient.close(); } + @Override + public boolean requiresLock() { + return true; + } + } \ No newline at end of file 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 f2792a84..adb8e7c7 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 @@ -103,4 +103,9 @@ public class IndexingCommand implements Command { ipcClient.close(); } + @Override + public boolean requiresLock() { + return true; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java index d6f1f4b6..4852d0e6 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java @@ -23,4 +23,9 @@ public class ListModelsCommand implements Command { + " " + entry.getValue().toString()); } + @Override + public boolean requiresLock() { + return false; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java index 2eba9b24..fa547c65 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java @@ -19,4 +19,9 @@ public class MessageCommand implements Command { client.close(); } + @Override + public boolean requiresLock() { + return false; + } + } 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 3bf6fa11..ca2627cf 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 @@ -1,5 +1,6 @@ package de.vipra.cmd.option; +import java.io.File; import java.io.IOException; import java.text.ParseException; @@ -54,6 +55,11 @@ public class ModelingCommand implements Command { @Override public void run() throws Exception { final Config config = Config.getConfig(); + if (config.getDtmPath() == null || config.getDtmPath().isEmpty()) + throw new Exception("dtm path not configured, set config variable 'dtmPath'"); + if (!new File(config.getDtmPath()).exists()) + throw new Exception("dtm not found at specified path: '" + config.getDtmPath() + "'"); + final IPCClient ipcClient = new IPCClient(); for (final TopicModelConfig modelConfig : config.getTopicModelConfigs(models)) { ipcClient.send(new IPCMessage(IPCMessageCode.START_MODELING).message("Started generating model '" + modelConfig.getName() + "'")); @@ -63,4 +69,9 @@ public class ModelingCommand implements Command { ipcClient.close(); } + @Override + public boolean requiresLock() { + return true; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java index e315b3c0..b335c621 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java @@ -26,4 +26,9 @@ public class PrintModelCommand implements Command { } } + @Override + public boolean requiresLock() { + return false; + } + } diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java index d3d51d54..388bd633 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java @@ -71,10 +71,15 @@ public class TestCommand implements Command { } ConsoleUtils.info("all tests passed"); - } catch (Exception e) { + } catch (final Exception e) { ConsoleUtils.print(Ansi.ansi().fg(Color.RED).a("FAILED").reset().toString()); throw e; } } + @Override + public boolean requiresLock() { + return false; + } + } diff --git a/vipra-ui/app/html/about.html b/vipra-ui/app/html/about.html index 4865c49c..cd457011 100644 --- a/vipra-ui/app/html/about.html +++ b/vipra-ui/app/html/about.html @@ -85,7 +85,16 @@ </tr> <tr class="well"> <td colspan="2"> - Analyzer, text processor and dynamic window resolution. + The dynamic topic modeling window resolution to be used. This value is only used, if the selected analyzer supports dynamic topic modeling. + </td> + </tr> + <tr> + <th>Processor mode</th> + <td ng-bind-template="{{::info.const.procmode}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + The processor mode defines the processed text output. In text mode, the text is trimmed down, in entity mode, the text is scanned for entities. In mixed mode, the found entities are inserted into the trimmed text. </td> </tr> <tr> @@ -128,6 +137,15 @@ The number of topics to be generated in the topic modeling process. </td> </tr> + <tr> + <th>K top words</th> + <td ng-bind-template="{{::info.const.ktopwords}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + The maximum number of top words per sequence and topic. + </td> + </tr> <tr> <th>Rising decay weight</th> <td ng-bind-template="{{::info.const.decaylambda}}"></td> @@ -137,6 +155,15 @@ A weight to the rising decay caulculation of topic relevances. The higher this value, the more focus is put on later sequences containing more recent documents. </td> </tr> + <tr> + <th>Minimum topic share</th> + <td ng-bind-template="{{::info.const.mintopicshare}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + Minimum topic share for an article. Topics with a smaller share are ignored. + </td> + </tr> <tr> <th>Minimum relative probability</th> <td ng-bind-template="{{::info.const.minrelprob}}"></td> @@ -151,20 +178,33 @@ <th>Maximum similar documents</th> <td ng-bind-template="{{::info.const.maxsimdocs}}"></td> </tr> + <tr> + <th>Maximum similar topics</th> + <td ng-bind-template="{{::info.const.maxsimtopics}}"></td> + </tr> <tr class="well"> <td colspan="2"> - Maximum number of similar documents for each document. + Maximum number of similar documents/topics for each document/topic. </td> </tr> <tr> - <th>Maximum divergence</th> - <td ng-bind-template="{{::info.const.maxdiv}}"></td> + <th>Maximum similar documents divergence</th> + <td ng-bind-template="{{::info.const.maxdocdiv}}"></td> </tr> <tr class="well"> <td colspan="2"> Maximum divergence between a document and similar documents. Lower values mean more similar documents (less divergence). </td> </tr> + <tr> + <th>Maximum similar topics divergence</th> + <td ng-bind-template="{{::info.const.maxtopicdiv}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + Maximum divergence between a topic and similar topics. Lower values mean more similar topics (less divergence). + </td> + </tr> <tr> <th>Dynamic minimum iterations</th> <td ng-bind-template="{{::info.const.dynminiter}}"></td> @@ -200,6 +240,24 @@ The minimum article word frequency. Words that occurr less than this frequency are stripped from the article. </td> </tr> + <tr> + <th>Spotlight support</th> + <td ng-bind-template="{{::info.const.spotsupport}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + Minimum number of dbpedia inlinks for an entity annotation to be accepted. + </td> + </tr> + <tr> + <th>Spotlight confidence</th> + <td ng-bind-template="{{::info.const.spotconf}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + Disambiguation confidence. Eliminates top n percent of inconfident annotations. Ranges from 0 to 1. + </td> + </tr> <tr> <th>Regex disallowed chars</th> <td ng-bind-template="{{::info.const.charsdisallow}}"></td> diff --git a/vipra-ui/app/html/articles/index.html b/vipra-ui/app/html/articles/index.html index 88ca1fd8..7757ce3e 100644 --- a/vipra-ui/app/html/articles/index.html +++ b/vipra-ui/app/html/articles/index.html @@ -5,6 +5,17 @@ </div> </menu-affix> <div class="container"> + <div class="row row-spaced"> + <div class="col-md-8 text-center"> + <char-selector ng-model="articlesIndexModels.startChar"/> + </div> + <div class="col-md-4"> + <div class="btn-group btn-group-justified"> + <input type="text" class="form-control" ng-model="articlesIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}"> + <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="articlesIndexModels.contains=''"></span> + </div> + </div> + </div> <div class="row"> <div class="col-md-12"> <div class="panel panel-default"> diff --git a/vipra-ui/app/html/directives/char-selector.html b/vipra-ui/app/html/directives/char-selector.html new file mode 100644 index 00000000..c91239cd --- /dev/null +++ b/vipra-ui/app/html/directives/char-selector.html @@ -0,0 +1,29 @@ +<div class="char-selector"> + <a ng-class="{selected:!ngModel}" data-char="">All</a> + <a ng-class="{selected:ngModel==='A'}" data-char="A">A</a> + <a ng-class="{selected:ngModel==='B'}" data-char="B">B</a> + <a ng-class="{selected:ngModel==='C'}" data-char="C">C</a> + <a ng-class="{selected:ngModel==='D'}" data-char="D">D</a> + <a ng-class="{selected:ngModel==='E'}" data-char="E">E</a> + <a ng-class="{selected:ngModel==='F'}" data-char="F">F</a> + <a ng-class="{selected:ngModel==='G'}" data-char="G">G</a> + <a ng-class="{selected:ngModel==='H'}" data-char="H">H</a> + <a ng-class="{selected:ngModel==='I'}" data-char="I">I</a> + <a ng-class="{selected:ngModel==='J'}" data-char="J">J</a> + <a ng-class="{selected:ngModel==='K'}" data-char="K">K</a> + <a ng-class="{selected:ngModel==='L'}" data-char="L">L</a> + <a ng-class="{selected:ngModel==='M'}" data-char="M">M</a> + <a ng-class="{selected:ngModel==='N'}" data-char="N">N</a> + <a ng-class="{selected:ngModel==='O'}" data-char="O">O</a> + <a ng-class="{selected:ngModel==='P'}" data-char="P">P</a> + <a ng-class="{selected:ngModel==='Q'}" data-char="Q">Q</a> + <a ng-class="{selected:ngModel==='R'}" data-char="R">R</a> + <a ng-class="{selected:ngModel==='S'}" data-char="S">S</a> + <a ng-class="{selected:ngModel==='T'}" data-char="T">T</a> + <a ng-class="{selected:ngModel==='U'}" data-char="U">U</a> + <a ng-class="{selected:ngModel==='V'}" data-char="V">V</a> + <a ng-class="{selected:ngModel==='W'}" data-char="W">W</a> + <a ng-class="{selected:ngModel==='X'}" data-char="X">X</a> + <a ng-class="{selected:ngModel==='Y'}" data-char="Y">Y</a> + <a ng-class="{selected:ngModel==='Z'}" data-char="Z">Z</a> +</div> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/sequence-dropdown.html b/vipra-ui/app/html/directives/sequence-dropdown.html index a7866212..54e8dc89 100644 --- a/vipra-ui/app/html/directives/sequence-dropdown.html +++ b/vipra-ui/app/html/directives/sequence-dropdown.html @@ -2,5 +2,4 @@ <li class="nya-bs-option" nya-bs-option="sequence in sequences"> <a ng-bind="sequence.label"></a> </li> -</ol> -<button class="btn btn-sm btn-default" ng-click="doClear()" ng-show="showClear">Clear</button> \ No newline at end of file +</ol> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/window-dropdown.html b/vipra-ui/app/html/directives/window-dropdown.html new file mode 100644 index 00000000..78e0e8c8 --- /dev/null +++ b/vipra-ui/app/html/directives/window-dropdown.html @@ -0,0 +1,12 @@ +<div class="input-group"> + <ol class="nya-bs-select form-control" ng-model="ngModel" ng-class="{dropup:showDropup}" disabled="!windows"> + <li class="nya-bs-option" nya-bs-option="window in windows"> + <a ng-bind="window.label"></a> + </li> + </ol> + <span class="input-group-btn"> + <button class="btn btn-default selectclear nooutline" type="button"> + <span class="glyphicon glyphicon-remove-circle" ng-click="ngModel=''"></span> + </button> + </span> +</div> \ No newline at end of file diff --git a/vipra-ui/app/html/entities/index.html b/vipra-ui/app/html/entities/index.html index 2554c3f2..ba032878 100644 --- a/vipra-ui/app/html/entities/index.html +++ b/vipra-ui/app/html/entities/index.html @@ -5,6 +5,17 @@ </div> </menu-affix> <div class="container"> + <div class="row row-spaced"> + <div class="col-md-8 text-center"> + <char-selector ng-model="entitiesIndexModels.startChar"/> + </div> + <div class="col-md-4"> + <div class="btn-group btn-group-justified"> + <input type="text" class="form-control" ng-model="entitiesIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}"> + <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="entitiesIndexModels.contains=''"></span> + </div> + </div> + </div> <div class="row"> <div class="col-md-12"> <div class="panel panel-default"> diff --git a/vipra-ui/app/html/network.html b/vipra-ui/app/html/network.html index 88bd5c4f..83610c2c 100644 --- a/vipra-ui/app/html/network.html +++ b/vipra-ui/app/html/network.html @@ -1,25 +1,51 @@ <div ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'network'"> <div class="fullsize navpadding"> - <div class="graph-legend overlay"> - <div class="checkbox"> - <input type="checkbox" id="showArticles" ng-model="shown.articles"> - <label for="showArticles">Articles</label> + <div class="graph-legend overlay" ng-class="{collapsed:legendCollapsed}"> + <i class="collapser fa pointer pull-right" ng-class="{'fa-chevron-up':!legendCollapsed,'fa-chevron-down':legendCollapsed}" ng-click="legendCollapsed=!legendCollapsed"></i> + <div ng-hide="legendCollapsed"> + <div class="checkbox"> + <input type="checkbox" id="showArticles" ng-model="shown.articles"> + <label for="showArticles">Articles</label> + </div> + <div class="checkbox"> + <input type="checkbox" id="showSimilarArticles" ng-model="shown.similararticles"> + <label for="showSimilarArticles">Similar Articles</label> + </div> + <div class="checkbox"> + <input type="checkbox" id="showTopics" ng-model="shown.topics"> + <label for="showTopics">Topics</label> + </div> + <div class="checkbox"> + <input type="checkbox" id="showWords" ng-model="shown.words"> + <label for="showWords">Words</label> + </div> + <div> + Window + <window-dropdown ng-model="selectedWindow" windows="windows" /> + </div> + <div> + <div class="btn-group btn-group-justified"> + <input class="form-control" type="text" ng-model="searchNodes" ng-model-options="{debounce:300}" placeholder="Search..."> + <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="searchNodes=''"></span> + </div> + </div> + <div> + <table> + <tr> + <td> + <button class="btn btn-default" ng-click="reset()"> + Reset + </button> + </td> + <td> + <button class="btn btn-default" ng-click="fit()"> + Zoom to fit + </button> + </td> + </tr> + </table> + </div> </div> - <div class="checkbox"> - <input type="checkbox" id="showSimilarArticles" ng-model="shown.similararticles"> - <label for="showSimilarArticles">Similar Articles</label> - </div> - <div class="checkbox"> - <input type="checkbox" id="showTopics" ng-model="shown.topics"> - <label for="showTopics">Topics</label> - </div> - <div class="checkbox"> - <input type="checkbox" id="showWords" ng-model="shown.words"> - <label for="showWords">Words</label> - </div> - <button class="btn btn-default" ng-click="reset()"> - Reset - </button> </div> <div class="fullsize" id="visgraph"></div> </div> diff --git a/vipra-ui/app/html/topics/index.html b/vipra-ui/app/html/topics/index.html index 8d7c207d..d9e3b962 100644 --- a/vipra-ui/app/html/topics/index.html +++ b/vipra-ui/app/html/topics/index.html @@ -5,6 +5,17 @@ </div> </menu-affix> <div class="container"> + <div class="row row-spaced"> + <div class="col-md-8 text-center"> + <char-selector ng-model="topicsIndexModels.startChar"/> + </div> + <div class="col-md-4"> + <div class="btn-group btn-group-justified"> + <input type="text" class="form-control" ng-model="topicsIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}"> + <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="topicsIndexModels.contains=''"></span> + </div> + </div> + </div> <div class="row"> <div class="col-md-12"> <div class="panel panel-default"> diff --git a/vipra-ui/app/html/words/index.html b/vipra-ui/app/html/words/index.html index 60ca0395..34a4cac3 100644 --- a/vipra-ui/app/html/words/index.html +++ b/vipra-ui/app/html/words/index.html @@ -5,6 +5,17 @@ </div> </menu-affix> <div class="container"> + <div class="row row-spaced"> + <div class="col-md-8 text-center"> + <char-selector ng-model="wordsIndexModels.startChar"/> + </div> + <div class="col-md-4"> + <div class="btn-group btn-group-justified"> + <input type="text" class="form-control" ng-model="wordsIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}"> + <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="wordsIndexModels.contains=''"></span> + </div> + </div> + </div> <div class="row"> <div class="col-md-12"> <div class="panel panel-default"> diff --git a/vipra-ui/app/index.html b/vipra-ui/app/index.html index 18917c14..af9a4e37 100644 --- a/vipra-ui/app/index.html +++ b/vipra-ui/app/index.html @@ -40,6 +40,9 @@ <li ui-sref-active="active"> <a tabindex="0" ui-sref="explorer"><span class="mnemonic">E</span>xplorer</a> </li> + <li ui-sref-active="active"> + <a tabindex="0" ui-sref="network"><span class="mnemonic">N</span>etwork</a> + </li> <li ui-sref-active="active"> <a tabindex="0" ui-sref="articles"><span class="mnemonic">A</span>rticles</a> </li> @@ -47,7 +50,7 @@ <a tabindex="0" ui-sref="topics"><span class="mnemonic">T</span>opics</a> </li> <li ui-sref-active="active"> - <a tabindex="0" ui-sref="entities">E<span class="mnemonic">n</span>tities</a> + <a tabindex="0" ui-sref="entities">Ent<span class="mnemonic">i</span>ties</a> </li> <li ui-sref-active="active"> <a tabindex="0" ui-sref="words"><span class="mnemonic">W</span>ords</a> diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js index 25a3d9b9..2985c762 100644 --- a/vipra-ui/app/js/app.js +++ b/vipra-ui/app/js/app.js @@ -11,7 +11,6 @@ 'ngResource', 'ngSanitize', 'ngAnimate', - 'ngWebSocket', 'ui.router', 'cfp.hotkeys', 'nya.bootstrap.select', @@ -43,7 +42,7 @@ }); $stateProvider.state('network', { - url: '/network/:type/:id', + url: '/network?type&id', templateUrl: 'html/network.html', controller: 'NetworkController' }); @@ -201,7 +200,7 @@ } ]); - app.run(['$rootScope', '$state', '$websocket', function($rootScope, $state, $websocket) { + app.run(['$rootScope', '$state', 'AlertFactory', function($rootScope, $state, AlertFactory) { $rootScope.loading = {}; @@ -224,11 +223,38 @@ $rootScope.alerts = []; }); - var socket = $websocket(Vipra.config.websocketUrl); - - socket.onMessage(function(message) { - console.log(message.data); - }); + $rootScope.handleWSMessage = function(data) { + AlertFactory.showAlert(data.message, {time:7000}); + }; + + $rootScope.showMessage = function(msg) { + AlertFactory.showAlert(msg, {time:7000}); + }; + + window.vipraWS = function() { + var socket = new WebSocket(Vipra.config.websocketUrl); + + socket.onmessage = function(event) { + try { + var data = JSON.parse(event.data); + $rootScope.$apply(function() { + $rootScope.handleWSMessage(data); + }); + } catch(e) { + $rootScope.$apply(function() { + $rootScope.showMessage(event.data); + }); + } + }; + + socket.onclose = function() { + setTimeout(function() { + window.vipraWS(); + }, 5000); + }; + }; + + window.vipraWS(); }]); diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index 9f9e5d25..31325a87 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -2,7 +2,7 @@ * Vipra Application * Controllers ******************************************************************************/ -/* globals angular, Vipra, moment, vis, console, prompt, randomColor, Highcharts, $ */ +/* globals angular, Vipra, moment, vis, prompt, randomColor, Highcharts, $ */ (function() { "use strict"; @@ -107,6 +107,15 @@ } }); + hotkeys.add({ + combo: 'n', + description: 'Go to network', + callback: function() { + if ($scope.rootModels.topicModel && $state.current.name !== 'network') + $state.transitionTo('network'); + } + }); + hotkeys.add({ combo: 'a', description: 'Go to articles', @@ -126,7 +135,7 @@ }); hotkeys.add({ - combo: 'n', + combo: 'i', description: 'Go to entities', callback: function() { if ($scope.rootModels.topicModel && $state.current.name !== 'entities') @@ -151,14 +160,6 @@ } }); - hotkeys.add({ - combo: 'b', - description: 'Report a bug', - callback: function() { - $scope.reportBug(); - } - }); - $scope.showCheatSheet = hotkeys.toggleCheatSheet; } ]); @@ -241,8 +242,8 @@ /** * Network controller */ - app.controller('NetworkController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory', 'TopicFactory', 'WordFactory', - function($scope, $state, $stateParams, $timeout, ArticleFactory, TopicFactory, WordFactory) { + app.controller('NetworkController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory', 'TopicFactory', 'WordFactory', 'WindowFactory', + function($scope, $state, $stateParams, $timeout, ArticleFactory, TopicFactory, WordFactory, WindowFactory) { var id = 0, ids = {}, @@ -253,13 +254,14 @@ topics: '#DBB234', words: '#FFFFFF' }; + $scope.nodes = new vis.DataSet(); $scope.edges = new vis.DataSet(); $scope.data = { nodes: $scope.nodes, edges: $scope.edges }; - $scope.type = $stateParams.type; + $scope.options = { nodes: { font: { @@ -287,6 +289,7 @@ hideEdgesOnDrag: true } }; + $scope.shown = { articles: true, similararticles: true, @@ -294,49 +297,6 @@ words: true }; - var factory; - if ($stateParams.type === 'articles') - factory = ArticleFactory; - else if ($stateParams.type === 'topics') - factory = TopicFactory; - else if ($stateParams.type === 'word') - factory = WordFactory; - else { - console.log('unknown network type'); - return; - } - - // if the topic model is not set, the page was refreshed - // set to true, id of current node decides topic model - if (!$scope.rootModels.topicModel) - $scope.rootModels.topicModel = true; - - // get root node - factory.get({ - id: $stateParams.id - }, function(data) { - $scope.rootNode = data; - - // add root node - if ($stateParams.type === 'articles') - $scope.nodes.add([articleNode(data)]); - else if ($stateParams.type === 'topics') - $scope.nodes.add([topicNode(data)]); - else if ($stateParams.type === 'words') - $scope.nodes.add([wordNode(data)]); - ids[data.id] = id; - - // create graph - var container = document.getElementById("visgraph"); - $scope.graph = new vis.Network(container, $scope.data, $scope.options); - $scope.graph.on('selectNode', $scope.select); - $scope.graph.on('doubleClick', $scope.open); - - // take topic model from node - if (!angular.isObject($scope.rootModels.topicModel)) - $scope.rootModels.topicModel = data.topicModel; - }); - var newNode = function(title, type, show, dbid, color, shape, loader) { ids[dbid] = ++id; return { @@ -348,11 +308,34 @@ dbid: dbid, shape: shape || 'dot', loader: loader, + borderWidth: 1, + origColor: { + background: color, + highlight: { + background: color, + } + }, color: { + border: '#000', background: color, highlight: { - background: color + background: color, } + }, + font: { + color: '#000' + } + }; + }; + + var newEdge = function(from, to) { + return { + from: from, + to: to, + selectionWidth: 1, + color: { + color: '#333', + highlight: '#f00' } }; }; @@ -396,20 +379,18 @@ for (var i = 0; i < result.length; i++) { var current = result[i]; if (ids.hasOwnProperty(current.id)) { - if (edgeExists(ids[current.id], node.id)) + if (node && edgeExists(ids[current.id], node.id)) continue; - newEdges.push({ - from: ids[current.id], - to: node.id - }); - addEdge(ids[current.id], node.id); + if(node) { + newEdges.push(newEdge(ids[current.id], node.id)); + addEdge(ids[current.id], node.id); + } } else { newNodes.push(nodeFunction(current)); - newEdges.push({ - from: id, - to: node.id - }); - addEdge(id, node.id); + if(node) { + newEdges.push(newEdge(id, node.id)); + addEdge(id, node.id); + } } } if (newNodes.length) @@ -497,16 +478,143 @@ }); }; + $scope.reset = function() { + $state.go($state.current, {}, { + reload: true + }); + }; + + $scope.fit = function() { + $scope.graph.fit({ + animation: { + offset: {x: 0, y: 0}, + duration: 1000, + easingFunction: 'easeInOutQuad' + } + }); + }; + $scope.$watch('rootModels.topicModel', function(newVal) { if ($scope.rootNode && $scope.rootNode.topicModel.id !== newVal.id) $state.transitionTo('index'); }); - $scope.reset = function() { - $state.go($state.current, {}, { - reload: true + $scope.$watch('searchNodes', function() { + var nodes = $scope.nodes.get(); + var i; + var updates = []; + if($scope.searchNodes) { + for(i = 0; i < nodes.length; i++) { + if(nodes[i].title.indexOf($scope.searchNodes) != -1) { + updates.push({ + id: nodes[i].id, + color: nodes[i].origColor, + font: { + color: '#000' + } + }); + } else { + updates.push({ + id: nodes[i].id, + color: { + background: '#eee' + }, + font: { + color: '#ccc' + } + }); + } + } + } else { + for(i = 0; i < nodes.length; i++) { + updates.push({ + id: nodes[i].id, + color: nodes[i].origColor, + font: { + color: '#000' + } + }); + } + } + + $scope.data.nodes.update(updates); + }); + + // create graph + var container = document.getElementById("visgraph"); + $scope.graph = new vis.Network(container, $scope.data, $scope.options); + $scope.graph.on('selectNode', $scope.select); + $scope.graph.on('doubleClick', $scope.open); + + if($stateParams.type) { + // if the topic model is not set, the page was refreshed + // set to true, id of current node decides topic model + if (!$scope.rootModels.topicModel) + $scope.rootModels.topicModel = true; + + // type is given, load node + var factory; + if ($stateParams.type === 'articles') + factory = ArticleFactory; + else if ($stateParams.type === 'topics') + factory = TopicFactory; + else if ($stateParams.type === 'word') + factory = WordFactory; + + // get root node + factory.get({ + id: $stateParams.id + }, function(data) { + $scope.rootNode = data; + + // add root node + if ($stateParams.type === 'articles') + $scope.nodes.add([articleNode(data)]); + else if ($stateParams.type === 'topics') + $scope.nodes.add([topicNode(data)]); + else if ($stateParams.type === 'words') + $scope.nodes.add([wordNode(data)]); + ids[data.id] = id; + + // take topic model from node + if (!angular.isObject($scope.rootModels.topicModel)) + $scope.rootModels.topicModel = data.topicModel; }); - }; + } else { + $scope.queryTopics = function() { + if ($scope.shown.topics) { + TopicFactory.query({ + topicModel: $scope.rootModels.topicModel.id + }, function(data) { + constructor(data, null, topicNode); + }); + } + }; + + $scope.$watch('rootModels.topicModel', function() { + $scope.queryTopics(); + }); + + // page was reloaded, choose topic model + if (!$scope.rootModels.topicModel) + $scope.chooseTopicModel(); + else + $scope.queryTopics(); + } + + $scope.reloadWindows = function() { + if(!$scope.rootModels.topicModel) return; + + WindowFactory.query({ + topicModel: $scope.rootModels.topicModel.id + }, function(data) { + $scope.windows = data; + }); + } + + $scope.$watch('rootModels.topicModel', function() { + $scope.reloadWindows(); + }); } ]); @@ -759,21 +867,33 @@ limit: 100 }; - $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadArticles = function() { if (!$scope.rootModels.topicModel) return; ArticleFactory.query({ skip: ($scope.articlesIndexModels.page - 1) * $scope.articlesIndexModels.limit, limit: $scope.articlesIndexModels.limit, sort: ($scope.articlesIndexModels.sortdir ? '' : '-') + $scope.articlesIndexModels.sortkey, - topicModel: $scope.rootModels.topicModel.id + topicModel: $scope.rootModels.topicModel.id, + char: $scope.articlesIndexModels.startChar, + contains: $scope.articlesIndexModels.contains }, function(data, headers) { $scope.articles = data; $scope.articlesTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.articlesIndexModels.limit); }); + }; + + $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadArticles(); }); + $scope.$watchGroup(['articlesIndexModels.startChar', 'articlesIndexModels.contains'], function() { + if($scope.articlesIndexModels.page !== 1) + $scope.articlesIndexModels.page = 1; + else + $scope.reloadArticles(); + }); } ]); @@ -794,7 +914,6 @@ id: $stateParams.id }, function(data) { $scope.article = data; - $scope.article.text = $scope.article.text; $scope.articleDate = Vipra.formatDate($scope.article.date); $scope.articleCreated = Vipra.formatDateTime($scope.article.created); $scope.articleModified = Vipra.formatDateTime($scope.article.modified); @@ -930,21 +1049,33 @@ limit: 100 }; - $scope.$watchGroup(['topicsIndexModels.page', 'topicsIndexModels.sortkey', 'topicsIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadTopics = function() { if (!$scope.rootModels.topicModel) return; TopicFactory.query({ topicModel: $scope.rootModels.topicModel.id, skip: ($scope.topicsIndexModels.page - 1) * $scope.topicsIndexModels.limit, limit: $scope.topicsIndexModels.limit, - sort: ($scope.topicsIndexModels.sortdir ? '' : '-') + $scope.topicsIndexModels.sortkey + sort: ($scope.topicsIndexModels.sortdir ? '' : '-') + $scope.topicsIndexModels.sortkey, + char: $scope.topicsIndexModels.startChar, + contains: $scope.topicsIndexModels.contains }, function(data, headers) { $scope.topics = data; $scope.topicsTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.topicsTotal / $scope.topicsIndexModels.limit); }); + }; + + $scope.$watchGroup(['topicsIndexModels.page', 'topicsIndexModels.sortkey', 'topicsIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadTopics(); }); + $scope.$watchGroup(['topicsIndexModels.startChar', 'topicsIndexModels.contains'], function() { + if($scope.topicsIndexModels.page !== 1) + $scope.topicsIndexModels.page = 1; + else + $scope.reloadTopics(); + }); } ]); @@ -1164,21 +1295,33 @@ limit: 100 }; - $scope.$watchGroup(['entitiesIndexModels.page', 'entitiesIndexModels.sortkey', 'entitiesIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadEntities = function() { if (!$scope.rootModels.topicModel) return; EntityFactory.query({ topicModel: $scope.rootModels.topicModel.id, skip: ($scope.entitiesIndexModels.page - 1) * $scope.entitiesIndexModels.limit, limit: $scope.entitiesIndexModels.limit, - sort: ($scope.entitiesIndexModels.sortdir ? '' : '-') + $scope.entitiesIndexModels.sortkey + sort: ($scope.entitiesIndexModels.sortdir ? '' : '-') + $scope.entitiesIndexModels.sortkey, + char: $scope.entitiesIndexModels.startChar, + contains: $scope.entitiesIndexModels.contains }, function(data, headers) { $scope.entities = data; $scope.entitiesTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.entitiesTotal / $scope.entitiesIndexModels.limit); }); + }; + + $scope.$watchGroup(['entitiesIndexModels.page', 'entitiesIndexModels.sortkey', 'entitiesIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadEntities(); }); + $scope.$watchGroup(['entitiesIndexModels.startChar', 'entitiesIndexModels.contains'], function() { + if($scope.entitiesIndexModels.page !== 1) + $scope.entitiesIndexModels.page = 1; + else + $scope.reloadEntities(); + }); } ]); @@ -1252,21 +1395,33 @@ limit: 100 }; - $scope.$watchGroup(['wordsIndexModels.page', 'wordsIndexModels.sortkey', 'wordsIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadWords = function() { if (!$scope.rootModels.topicModel) return; - + WordFactory.query({ topicModel: $scope.rootModels.topicModel.id, skip: ($scope.wordsIndexModels.page - 1) * $scope.wordsIndexModels.limit, limit: $scope.wordsIndexModels.limit, - sort: ($scope.wordsIndexModels.sortdir ? '' : '-') + $scope.wordsIndexModels.sortkey + sort: ($scope.wordsIndexModels.sortdir ? '' : '-') + $scope.wordsIndexModels.sortkey, + char: $scope.wordsIndexModels.startChar, + contains: $scope.wordsIndexModels.contains }, function(data, headers) { $scope.words = data; $scope.wordsTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.wordsTotal / $scope.wordsIndexModels.limit); }); + }; + + $scope.$watchGroup(['wordsIndexModels.page', 'wordsIndexModels.sortkey', 'wordsIndexModels.sortdir', 'rootModels.topicModel'], function() { + $scope.reloadWords(); }); + $scope.$watchGroup(['wordsIndexModels.startChar', 'wordsIndexModels.contains'], function() { + if($scope.wordsIndexModels.page !== 1) + $scope.wordsIndexModels.page = 1; + else + $scope.reloadWords(); + }); } ]); diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index b41b9c9d..a2989a5e 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -266,15 +266,34 @@ } } }); - - $scope.doClear = function() { - delete $scope.ngModel; - }; }, templateUrl: '/html/directives/sequence-dropdown.html' }; }]); + app.directive('windowDropdown', [function() { + return { + scope: { + ngModel: '=', + windows: '=', + dropup: '@' + }, + link: function($scope) { + $scope.showDropup = $scope.dropup === 'true'; + + $scope.$watch('windows', function(newValue) { + if (newValue) { + for (var i = 0, w; i < $scope.windows.length; i++) { + w = $scope.windows[i]; + w.label = Vipra.windowLabel(w.startDate, w.windowResolution); + } + } + }); + }, + templateUrl: '/html/directives/window-dropdown.html' + }; + }]); + app.directive('sortBy', [function() { return { restrict: 'A', @@ -471,4 +490,22 @@ }; }]); + app.directive('charSelector', [function() { + return { + replace: true, + scope: { + ngModel: '=' + }, + link: function($scope, $elem) { + $elem.on('click', 'a', function() { + var c = $(this).data('char'); + $scope.$apply(function() { + $scope.ngModel = c; + }); + }); + }, + templateUrl: 'html/directives/char-selector.html' + }; + }]); + })(); \ No newline at end of file diff --git a/vipra-ui/app/js/factories.js b/vipra-ui/app/js/factories.js index 514dc1cb..430f6af6 100644 --- a/vipra-ui/app/js/factories.js +++ b/vipra-ui/app/js/factories.js @@ -2,7 +2,7 @@ * Vipra Application * Factories ******************************************************************************/ -/* globals angular, Vipra */ +/* globals angular, Vipra, $ */ (function() { "use strict"; @@ -84,4 +84,39 @@ return $myResource(Vipra.config.restUrl + '/entities/:id'); }]); + app.factory('WindowFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/windows/:id'); + }]); + + app.factory('AlertFactory', [function() { + var alerts = $("#alerts"); + if(alerts.length === 0) { + alerts = $('<div id="alerts" class="alerts"></div>').appendTo('body'); + } + + function showAlert(msg, config) { + config = angular.merge({}, { + type: 'info', + time: 0, + dismissible: true + }, config); + var classes = 'alert alert-' + config.type; + if(config.dismissible) { + classes += ' alert-dismissible'; + } + var alert = $('<div class="' + classes + '" role="alert" style="display:none">' + (config.dismissible ? + '<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>' : '') + + msg + '</div>').appendTo(alerts).fadeIn(); + if(config.time > 0) { + setTimeout(function() { + alert.fadeOut(300, function() { $(this).remove(); }); + }, config.time); + } + } + + return { + showAlert: showAlert + }; + }]); + })(); \ No newline at end of file diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index 4534a006..cb294ea3 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -86,24 +86,37 @@ a:hover { } .graph-legend { + @spacing: 5px; position: absolute; top: 10px; left: 10px; - font-weight: bold; - padding: 10px; - background: rgba(255, 255, 255, 0.75); + padding: @spacing*2; + background: rgba(128, 128, 128, 0.1); border-radius: 5px; - label { - margin: 0; + &:not(.collapsed) { + width: 220px; } label + label { - padding-left: 5px; + padding-left: @spacing; } - .checkbox:first-child { - margin-top: 0; + label, + .checkbox { + margin: 0; + } + div + div { + margin-top: @spacing; } - .checkbox:last-child { - margin-bottom: 0; + table, + table button { + width: 100%; + td + td { + padding-left: @spacing; + } + } + .collapser { + margin: 0; + position: relative; + z-index: 2; } } @@ -437,6 +450,20 @@ a:hover { color: #ccc; } +.selectclear, +.selectclear:hover, +.selectclear:active, +.selectclear:focus, +.selectclear:active:hover { + border-left: 0; + padding-left: 4px; + padding-right: 4px; + color: #ccc; + background: #fff; + outline: none; + border-color: #ccc; +} + .chart, .highcharts-container { width: 100% !important; @@ -522,6 +549,7 @@ entity-menu { bottom: 20px; left: 20px; right: 20px; + z-index: 8999; .alert { margin: 0; @@ -755,6 +783,31 @@ entity-menu { display: block !important; } +.char-selector { + @char-padding: 5px; + padding: 5px; + text-align: center; + font-size: 0; + > a { + font-size: 14px; + padding-right: @char-padding; + } + > a:last-child { + padding-right: 0; + } + > a + a { + padding-left: @char-padding; + border-left: 1px solid #ccc; + } + > a.selected { + text-decoration: underline; + } +} + +.nooutline:focus { + outline: none; +} + @-moz-keyframes spin { 100% { -moz-transform: rotateY(360deg); diff --git a/vipra-ui/bower.json b/vipra-ui/bower.json index 1d60c9d9..e29148b2 100644 --- a/vipra-ui/bower.json +++ b/vipra-ui/bower.json @@ -23,7 +23,6 @@ "angular-resource": "^1.x", "angular-sanitize": "^1.x", "angular-animate": "^1.x", - "angular-websocket": "^1.x", "angular-ui-router": "^0.x", "highcharts": "^4.x", "vis": "^4.x", diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js index 6d8a564a..90043934 100644 --- a/vipra-ui/gulpfile.js +++ b/vipra-ui/gulpfile.js @@ -17,7 +17,6 @@ var assets = { 'bower_components/angular-resource/angular-resource.min.js', 'bower_components/angular-sanitize/angular-sanitize.min.js', 'bower_components/angular-animate/angular-animate.min.js', - 'bower_components/angular-websocket/angular-websocket.min.js', 'bower_components/angular-hotkeys/build/hotkeys.min.js', 'bower_components/angular-ui-router/release/angular-ui-router.min.js', 'bower_components/bootstrap/dist/js/bootstrap.min.js', diff --git a/vipra-util/src/main/java/de/vipra/util/Config.java b/vipra-util/src/main/java/de/vipra/util/Config.java index 55fec90e..3c7c8aaa 100644 --- a/vipra-util/src/main/java/de/vipra/util/Config.java +++ b/vipra-util/src/main/java/de/vipra/util/Config.java @@ -237,7 +237,7 @@ public class Config { return getConfig(false); } - public static Config getConfig(boolean skipModels) throws ConfigException { + public static Config getConfig(final boolean skipModels) throws ConfigException { if (instance == null) { try { InputStream in = null; 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 c751672e..089276a5 100644 --- a/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java +++ b/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java @@ -96,7 +96,7 @@ public class ConsoleUtils { public static int print(final String msg) { return print(System.out, true, msg); } - + public static int printNOLF(final String msg) { return print(System.out, false, msg); } diff --git a/vipra-util/src/main/java/de/vipra/util/Constants.java b/vipra-util/src/main/java/de/vipra/util/Constants.java index edc7792a..dbee71c2 100644 --- a/vipra-util/src/main/java/de/vipra/util/Constants.java +++ b/vipra-util/src/main/java/de/vipra/util/Constants.java @@ -107,6 +107,11 @@ public class Constants { */ public static final int MAX_SIMILAR_DOCUMENTS = 10; + /** + * Maximum number of similar topics for each topic. Default 10. + */ + public static final int MAX_SIMILAR_TOPICS = 10; + /** * Maximum divergence between a document and similar documents. Lower values * mean more similar documents (less divergence). Default 0.25. diff --git a/vipra-util/src/main/java/de/vipra/util/IPCClient.java b/vipra-util/src/main/java/de/vipra/util/IPCClient.java index 46fd449d..2d6c50e4 100644 --- a/vipra-util/src/main/java/de/vipra/util/IPCClient.java +++ b/vipra-util/src/main/java/de/vipra/util/IPCClient.java @@ -10,21 +10,20 @@ import de.vipra.util.ex.ConfigException; public class IPCClient implements Closeable { - private final Socket socket; - private final DataOutputStream out; + private final Config config = Config.getConfig(); + private Socket socket; + private DataOutputStream out; public IPCClient() throws ConfigException, UnknownHostException, IOException { - final Config config = Config.getConfig(); - socket = new Socket(config.getIpcHost(), config.getIpcPort()); - out = new DataOutputStream(socket.getOutputStream()); + reconnect(); } public IPCClient send(final IPCMessage message) throws IOException { - if (!socket.isClosed()) { - final String messageStr = Config.mapper.writeValueAsString(message); - out.writeUTF(messageStr); - out.flush(); - } + if (socket.isClosed()) + reconnect(); + final String messageStr = Config.mapper.writeValueAsString(message); + out.writeUTF(messageStr); + out.flush(); return this; } @@ -33,4 +32,10 @@ public class IPCClient implements Closeable { socket.close(); } + private void reconnect() throws UnknownHostException, IOException { + socket = new Socket(config.getIpcHost(), config.getIpcPort()); + socket.setKeepAlive(true); + out = new DataOutputStream(socket.getOutputStream()); + } + } diff --git a/vipra-util/src/main/java/de/vipra/util/LockFile.java b/vipra-util/src/main/java/de/vipra/util/LockFile.java index 21698934..b68de7f6 100644 --- a/vipra-util/src/main/java/de/vipra/util/LockFile.java +++ b/vipra-util/src/main/java/de/vipra/util/LockFile.java @@ -7,10 +7,10 @@ import de.vipra.util.ex.LockFileException; public class LockFile { - private File file = new File(PathUtils.tempDir(), "vipra.lock"); + private final File file = new File(PathUtils.tempDir(), "vipra.lock"); public void lock() throws LockFileException { - if (file.exists()) + if (isLocked()) throw new LockFileException(); try { file.createNewFile(); @@ -27,6 +27,10 @@ public class LockFile { } } + public boolean isLocked() { + return file.exists(); + } + public String getPath() { return file.getAbsolutePath(); } diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java b/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java index d369d5ef..30f28c73 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java +++ b/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java @@ -33,6 +33,7 @@ public class TopicModelConfig implements Serializable { private int staticIterations = Constants.STATIC_ITER; private int topicAutoNamingWords = Constants.TOPIC_AUTO_NAMING_WORDS; private int maxSimilarDocuments = Constants.MAX_SIMILAR_DOCUMENTS; + private int maxSimilarTopics = Constants.MAX_SIMILAR_TOPICS; private int documentMinimumLength = Constants.DOCUMENT_MIN_LENGTH; private int documentMinimumWordFrequency = Constants.DOCUMENT_MIN_WORD_FREQ; private int spotlightSupport = Constants.SPOTLIGHT_SUPPORT; @@ -55,6 +56,7 @@ public class TopicModelConfig implements Serializable { staticIterations = topicModelConfig.getStaticIterations(); topicAutoNamingWords = topicModelConfig.getTopicAutoNamingWords(); maxSimilarDocuments = topicModelConfig.getMaxSimilarDocuments(); + maxSimilarTopics = topicModelConfig.getMaxSimilarTopics(); documentMinimumLength = topicModelConfig.getDocumentMinimumLength(); documentMinimumWordFrequency = topicModelConfig.getDocumentMinimumWordFrequency(); spotlightSupport = topicModelConfig.getSpotlightSupport(); @@ -156,6 +158,14 @@ public class TopicModelConfig implements Serializable { this.maxSimilarDocuments = maxSimilarDocuments; } + public int getMaxSimilarTopics() { + return maxSimilarTopics; + } + + public void setMaxSimilarTopics(int maxSimilarTopics) { + this.maxSimilarTopics = maxSimilarTopics; + } + public int getSpotlightSupport() { return spotlightSupport; } diff --git a/vipra-util/src/main/java/de/vipra/util/service/MongoService.java b/vipra-util/src/main/java/de/vipra/util/service/MongoService.java index b7ea2dce..26fda9a7 100644 --- a/vipra-util/src/main/java/de/vipra/util/service/MongoService.java +++ b/vipra-util/src/main/java/de/vipra/util/service/MongoService.java @@ -208,7 +208,7 @@ public class MongoService<Type extends Model<IdType>, IdType> { return datastore.getCount(clazz); final Query<Type> query = datastore.createQuery(clazz); - builder.build(query); + builder.build(query, true); return datastore.getCount(query); } diff --git a/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java b/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java index 75875fd0..e7a587de 100644 --- a/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java +++ b/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java @@ -3,17 +3,26 @@ package de.vipra.util.service; import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import org.mongodb.morphia.query.Query; public class QueryBuilder { public static enum CriterionType { - EQ, - LT, - LTE, - GT, - GTE + EQUALS, + EQUALS_IGNORECASE, + LESSERTHAN, + LESSERTHANEQUAL, + GREATERTHAN, + GREATERTHANEQUAL, + STARTSWITH, + STARTSWITH_IGNORECASE, + ENDSWITH, + ENDSWITH_IGNORECASE, + CONTAINS, + CONTAINS_IGNORECASE, + REGEX }; public static class Criterion { @@ -29,21 +38,44 @@ public class QueryBuilder { public void apply(final Query<?> query) { switch (type) { - case EQ: + case EQUALS: query.field(field).equal(value); break; - case LT: + case EQUALS_IGNORECASE: + query.field(field).equalIgnoreCase(value); + break; + case LESSERTHAN: query.field(field).lessThan(value); break; - case LTE: + case LESSERTHANEQUAL: query.field(field).lessThanOrEq(value); break; - case GT: + case GREATERTHAN: query.field(field).greaterThan(value); break; - case GTE: + case GREATERTHANEQUAL: query.field(field).greaterThanOrEq(value); break; + case STARTSWITH: + query.field(field).startsWith((String) value); + break; + case STARTSWITH_IGNORECASE: + query.field(field).startsWithIgnoreCase((String) value); + break; + case ENDSWITH: + query.field(field).endsWith((String) value); + break; + case ENDSWITH_IGNORECASE: + query.field(field).endsWithIgnoreCase((String) value); + break; + case CONTAINS: + query.field(field).contains((String) value); + break; + case CONTAINS_IGNORECASE: + query.field(field).containsIgnoreCase((String) value); + break; + case REGEX: + query.filter(field, value); } } } @@ -83,32 +115,48 @@ public class QueryBuilder { } public QueryBuilder eq(final String field, final Object value) { - if (field != null && !field.isEmpty() && value != null) - criteria.add(new Criterion(field, CriterionType.EQ, value)); - return this; + return criterion(field, CriterionType.EQUALS, value); + } + + public QueryBuilder eq(final String field, final Object value, final boolean ignoreCase) { + return criterion(field, ignoreCase ? CriterionType.EQUALS_IGNORECASE : CriterionType.EQUALS, value); } public QueryBuilder lt(final String field, final Object value) { - if (field != null && !field.isEmpty() && value != null) - criteria.add(new Criterion(field, CriterionType.LT, value)); - return this; + return criterion(field, CriterionType.LESSERTHAN, value); } public QueryBuilder lte(final String field, final Object value) { - if (field != null && !field.isEmpty() && value != null) - criteria.add(new Criterion(field, CriterionType.LTE, value)); - return this; + return criterion(field, CriterionType.LESSERTHANEQUAL, value); } public QueryBuilder gt(final String field, final Object value) { - if (field != null && !field.isEmpty() && value != null) - criteria.add(new Criterion(field, CriterionType.GT, value)); - return this; + return criterion(field, CriterionType.GREATERTHAN, value); } public QueryBuilder gte(final String field, final Object value) { + return criterion(field, CriterionType.GREATERTHANEQUAL, value); + } + + public QueryBuilder startsWith(final String field, final String value, final boolean ignoreCase) { + return criterion(field, ignoreCase ? CriterionType.STARTSWITH_IGNORECASE : CriterionType.STARTSWITH, value); + } + + public QueryBuilder endsWith(final String field, final String value, final boolean ignoreCase) { + return criterion(field, ignoreCase ? CriterionType.ENDSWITH_IGNORECASE : CriterionType.ENDSWITH, value); + } + + public QueryBuilder contains(final String field, final String value, final boolean ignoreCase) { + return criterion(field, ignoreCase ? CriterionType.CONTAINS_IGNORECASE : CriterionType.CONTAINS, value); + } + + public QueryBuilder regex(final String field, final Pattern pattern) { + return criterion(field, CriterionType.REGEX, pattern); + } + + private QueryBuilder criterion(final String field, final CriterionType type, final Object value) { if (field != null && !field.isEmpty() && value != null) - criteria.add(new Criterion(field, CriterionType.GTE, value)); + criteria.add(new Criterion(field, type, value)); return this; } @@ -168,11 +216,15 @@ public class QueryBuilder { } public Query<?> build(final Query<?> query) { - if (getSkip() != null && getSkip() > 0) + return build(query, false); + } + + public Query<?> build(final Query<?> query, final boolean counting) { + if (!counting && getSkip() != null && getSkip() > 0) query.offset(getSkip()); - if (getLimit() != null && getLimit() > 0) + if (!counting && getLimit() != null && getLimit() > 0) query.limit(getLimit()); - if (getSortBy() != null) + if (!counting && getSortBy() != null) query.order(getSortBy()); if (getCriteria() != null) for (final Criterion criterion : getCriteria()) -- GitLab