From effa30c7eeef7ca5c96af149f2d1f18ff8ba067a Mon Sep 17 00:00:00 2001 From: Eike Cochu <eike@cochu.com> Date: Thu, 17 Mar 2016 00:31:17 +0100 Subject: [PATCH] added ArticleWord model, removed unused FileModel removed filemodel, unused now storing words with word count per article moved sort direction link to directive --- .../vipra/rest/resource/ArticleResource.java | 24 +++++----- .../de/vipra/rest/resource/TopicResource.java | 22 +-------- .../de/vipra/cmd/option/ImportCommand.java | 1 + .../java/de/vipra/cmd/text/ProcessedText.java | 20 ++++++++ vipra-ui/app/html/articles/index.html | 8 +--- vipra-ui/app/html/directives/sort-dir.html | 1 + vipra-ui/app/html/directives/topic-link.html | 6 +-- vipra-ui/app/html/directives/topic-menu.html | 2 +- vipra-ui/app/html/explorer.html | 13 +++-- vipra-ui/app/html/topics/articles.html | 8 +--- vipra-ui/app/html/topics/index.html | 8 +--- vipra-ui/app/js/controllers.js | 25 +++++----- vipra-ui/app/js/directives.js | 16 ++++++- vipra-ui/app/less/app.less | 19 +++++++- .../java/de/vipra/util/model/ArticleFull.java | 29 +++++------ .../java/de/vipra/util/model/ArticleWord.java | 48 +++++++++++++++++++ .../java/de/vipra/util/model/FileModel.java | 21 -------- 17 files changed, 158 insertions(+), 113 deletions(-) create mode 100644 vipra-ui/app/html/directives/sort-dir.html create mode 100644 vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java delete mode 100644 vipra-util/src/main/java/de/vipra/util/model/FileModel.java 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 acc0312b..e235dc29 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 @@ -35,6 +35,7 @@ import de.vipra.util.ex.ConfigException; import de.vipra.util.ex.DatabaseException; import de.vipra.util.model.ArticleFull; import de.vipra.util.service.MongoService; +import de.vipra.util.service.Service.QueryBuilder; @Path("articles") public class ArticleResource { @@ -52,15 +53,22 @@ public class ArticleResource { @GET @Produces(MediaType.APPLICATION_JSON) public Response getArticles(@QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, - @QueryParam("sort") @DefaultValue("date") final String sortBy, @QueryParam("fields") final String fields) { + @QueryParam("sort") @DefaultValue("date") final String sortBy, @QueryParam("fields") final String fields, + @QueryParam("word") final String word) { final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>(); if (res.hasErrors()) return res.badRequest(); try { - final List<ArticleFull> articles = dbArticles.getMultiple(skip, limit, sortBy, - StringUtils.getFields(fields)); + final QueryBuilder query = QueryBuilder.builder().skip(skip).limit(limit).sortBy(sortBy); + if (fields != null && !fields.isEmpty()) + query.fields(true, StringUtils.getFields(fields)); + + if (word != null && !word.isEmpty()) + query.criteria("words.word.id", word); + + final List<ArticleFull> articles = dbArticles.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) res.addHeader("total", dbArticles.count(null)); @@ -105,16 +113,6 @@ public class ArticleResource { } } - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("{id}/similar") - public Response getSimilar(@PathParam("id") final String id, @QueryParam("skip") final Integer skip, - @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("date") final String sortBy, - @QueryParam("fields") final String fields) { - // TODO implement - return null; - } - @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) 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 69d76ea7..c7f665a9 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 @@ -105,7 +105,7 @@ public class TopicResource { @Path("{id}/articles") public Response getArticles(@PathParam("id") final String id, @QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("title") final String sortBy, - @QueryParam("fields") final String fields, @Context UriInfo uriInfo) { + @QueryParam("fields") final String fields, @Context final UriInfo uriInfo) { final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>(); try { final Topic topic = new Topic(MongoUtils.objectId(id)); @@ -129,26 +129,6 @@ public class TopicResource { } } - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("{id}/similar/by-words") - public Response similarTopicsByWords(@PathParam("id") final String id, @QueryParam("skip") final Integer skip, - @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("title") final String sortBy, - @QueryParam("fields") final String fields) { - // TODO implement - return null; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("{id}/similar/by-articles") - public Response similarTopicsByArticles(@PathParam("id") final String id, @QueryParam("skip") final Integer skip, - @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("title") final String sortBy, - @QueryParam("fields") final String fields) { - // TODO implement - return null; - } - @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) 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 8e17e0e6..4cc9527a 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 @@ -83,6 +83,7 @@ public class ImportCommand implements Command { // preprocess text final ProcessedText processedText = processor.process(article.getText()); article.setProcessedText(processedText.getWords()); + article.setWords(processedText.getArticleWords()); // generate article stats final ArticleStats stats = new ArticleStats(); diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java b/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java index a47d1738..8911bcd2 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java @@ -1,17 +1,33 @@ package de.vipra.cmd.text; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import de.vipra.util.CountMap; +import de.vipra.util.model.ArticleWord; + public class ProcessedText { private final String[] words; private final long originalWordCount; private final long reducedWordCount; private final double reductionRatio; + private final List<ArticleWord> articleWords; public ProcessedText(final String text, final long wordCount) { words = text.split("\\s+"); originalWordCount = wordCount; reducedWordCount = words.length; reductionRatio = 1 - ((double) reducedWordCount / wordCount); + + final CountMap<String> wordCounts = new CountMap<>(); + for (final String word : words) + wordCounts.count(word); + final List<ArticleWord> articleWords = new ArrayList<>(wordCounts.size()); + for (final Entry<String, Integer> entry : wordCounts.entrySet()) + articleWords.add(new ArticleWord(entry.getKey(), entry.getValue())); + this.articleWords = articleWords; } public String[] getWords() { @@ -30,4 +46,8 @@ public class ProcessedText { return reductionRatio; } + public List<ArticleWord> getArticleWords() { + return articleWords; + } + } diff --git a/vipra-ui/app/html/articles/index.html b/vipra-ui/app/html/articles/index.html index b37d4ad6..681ebf3e 100644 --- a/vipra-ui/app/html/articles/index.html +++ b/vipra-ui/app/html/articles/index.html @@ -12,16 +12,12 @@ <ng-pluralize count="articlesTotal||0" when="{0:'no articles',1:'1 article',other:'{} articles'}"></ng-pluralize> in the database. <span ng-show="articlesTotal"> Sort by - <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.sort"> + <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.sortkey"> <li value="title" class="nya-bs-option"><a>Title</a></li> <li value="date" class="nya-bs-option"><a>Date</a></li> <li value="created" class="nya-bs-option"><a>Added</a></li> </ol> - Direction - <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.order"> - <li value="+" class="nya-bs-option"><a>Ascending</a></li> - <li value="-" class="nya-bs-option"><a>Descending</a></li> - </ol> + <sort-dir ng-model="opts.sortdir" /> </span> </div> <table class="table table-hover table-condensed"> diff --git a/vipra-ui/app/html/directives/sort-dir.html b/vipra-ui/app/html/directives/sort-dir.html new file mode 100644 index 00000000..155e996f --- /dev/null +++ b/vipra-ui/app/html/directives/sort-dir.html @@ -0,0 +1 @@ +<i class="pointer fa" ng-class="{'fa-sort-amount-desc':ngModel,'fa-sort-amount-asc':!ngModel}" ng-click="ngModel=!ngModel; $event.stopPropagation()"></i> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/topic-link.html b/vipra-ui/app/html/directives/topic-link.html index 3423ff2b..49ccb9e2 100644 --- a/vipra-ui/app/html/directives/topic-link.html +++ b/vipra-ui/app/html/directives/topic-link.html @@ -1,7 +1,7 @@ <span> <a class="topic-link" ui-sref="topics.show({id:topic.id})"> <span ng-bind="topic.name"></span> -<ng-transclude/> -</a> -<topic-menu topic="topic" /> + <ng-transclude/> + </a> + <topic-menu topic="topic" right="true" /> </span> diff --git a/vipra-ui/app/html/directives/topic-menu.html b/vipra-ui/app/html/directives/topic-menu.html index 8d3b33b2..f08e2a20 100644 --- a/vipra-ui/app/html/directives/topic-menu.html +++ b/vipra-ui/app/html/directives/topic-menu.html @@ -2,7 +2,7 @@ <a data-toggle="dropdown"> <i class="fa fa-caret-down"></i> </a> - <ul class="dropdown-menu dropdown-menu-right"> + <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}"> <li><a ui-sref="topics.show({id:topic.id})">Show</a></li> <li><a ui-sref="network({type:'topics',id:topic.id})">Network</a></li> <li><a ui-sref="topics.show.articles({id:topic.id})">Articles</a></li> diff --git a/vipra-ui/app/html/explorer.html b/vipra-ui/app/html/explorer.html index a0ea30ec..f6d7e25a 100644 --- a/vipra-ui/app/html/explorer.html +++ b/vipra-ui/app/html/explorer.html @@ -12,8 +12,8 @@ <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'fallingRelevance'" title="Sort by falling relevance">↘</a> <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'risingRelevance'" title="Sort by rising relevance">↗</a> <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'risingDecayRelevance'" title="Sort by rising relevance with decay">↝</a> - <a class="btn btn-sm btn-link" ng-click="opts.sortdir=!opts.sortdir"> - <i class="fa" ng-class="{'fa-sort-amount-desc':opts.sortdir,'fa-sort-amount-asc':!opts.sortdir}"></i> + <a class="btn btn-sm btn-link btn-plain" ng-click="opts.sortdir=!opts.sortdir"> + <sort-dir ng-model="opts.sortdir" /> </a> </div> <div class="btn-group btn-group-justified"> @@ -22,13 +22,16 @@ </div> <ul class="list-unstyled topic-choice"> <li ng-repeat="topic in topics | orderBy:opts.sorttopics:opts.sortdir | filter:search"> - <div class="checkbox checkbox-condensed" ng-class="{selected:topic.selected}" bs-popover popover-title="{{::topic.name}}" popover-template="partials/topic-popover.html"> + <div class="checkbox checkbox-condensed" ng-class="{selected:topic.selected}"> <span class="valuebar" ng-style="{width:topicCurrValue(topic)}"></span> <input type="checkbox" ng-model="topic.selected" ng-attr-id="{{::topic.id}}" ng-change="redrawGraph()"> <label class="check" ng-attr-for="{{::topic.id}}"> - <span class="ellipsis" ng-bind="::topic.name"></span> + <topic-menu topic="topic" /> + <span class="ellipsis topic"> + <span ng-bind="::topic.name"></span> + </span> </label> - <span class="colorbox" style="background:{{::topic.color}}"></span> + <span class="colorbox" style="background:{{::topic.color}}" bs-popover popover-title="{{::topic.name}}" popover-template="partials/topic-popover.html" popover-delay="500"></span> </div> </li> </ul> diff --git a/vipra-ui/app/html/topics/articles.html b/vipra-ui/app/html/topics/articles.html index c27bec67..c7ecd073 100644 --- a/vipra-ui/app/html/topics/articles.html +++ b/vipra-ui/app/html/topics/articles.html @@ -26,16 +26,12 @@ <ng-pluralize count="articlesTotal||0" when="{0:'no articles',1:'1 article',other:'{} articles'}"></ng-pluralize> in the database. <span ng-show="articlesTotal"> Sort by - <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.sort"> + <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.sortkey"> <li value="title" class="nya-bs-option"><a>Title</a></li> <li value="date" class="nya-bs-option"><a>Date</a></li> <li value="created" class="nya-bs-option"><a>Added</a></li> </ol> - Direction - <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.order"> - <li value="+" class="nya-bs-option"><a>Ascending</a></li> - <li value="-" class="nya-bs-option"><a>Descending</a></li> - </ol> + <sort-dir ng-model="opts.sortdir" /> </span> </div> <table class="table table-hover table-condensed"> diff --git a/vipra-ui/app/html/topics/index.html b/vipra-ui/app/html/topics/index.html index 413d3d26..3a8babc5 100644 --- a/vipra-ui/app/html/topics/index.html +++ b/vipra-ui/app/html/topics/index.html @@ -12,15 +12,11 @@ <ng-pluralize count="topicsTotal||0" when="{0:'no topics',1:'1 topic',other:'{} topics'}"></ng-pluralize> in the database. <span ng-show="topicsTotal"> Sort by - <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.sort"> + <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.sortkey"> <li value="name" class="nya-bs-option"><a>Name</a></li> <li value="created" class="nya-bs-option"><a>Added</a></li> </ol> - Direction - <ol class="nya-bs-select nya-bs-condensed" ng-model="opts.order"> - <li value="+" class="nya-bs-option"><a>Ascending</a></li> - <li value="-" class="nya-bs-option"><a>Descending</a></li> - </ol> + <sort-dir ng-model="opts.sortdir" /> </span> </div> <table class="table table-hover table-condensed"> diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index 8012e8e9..9c460c9d 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -307,6 +307,7 @@ $scope.opts = { sorttopics: 'name', + sortdir: false, seqstyle: 'absolute', chartstyle: 'areaspline', chartstack: 'none' @@ -421,18 +422,18 @@ function($scope, $state, $location, ArticleFactory) { $scope.opts = { - sort: 'date', - order: '+' + sortkey: 'date', + sortdir: true }; $scope.page = Math.max($location.search().page || 1, 1); $scope.limit = 100; - $scope.$watchGroup(['page', 'opts.sort', 'opts.order'], function() { + $scope.$watchGroup(['page', 'opts.sortkey', 'opts.sortdir'], function() { ArticleFactory.query({ skip: ($scope.page - 1) * $scope.limit, limit: $scope.limit, - sort: $scope.opts.order + $scope.opts.sort + sort: ($scope.opts.sortdir ? '' : '-') + $scope.opts.sortkey }, function(data, headers) { $scope.articles = data; $scope.articlesTotal = headers("V-Total"); @@ -520,18 +521,18 @@ function($scope, $location, TopicFactory) { $scope.opts = { - sort: 'name', - order: '+' + sortkey: 'name', + sortdir: true }; $scope.page = Math.max($location.search().page || 1, 1); $scope.limit = 100; - $scope.$watchGroup(['page', 'opts.sort', 'opts.order'], function() { + $scope.$watchGroup(['page', 'opts.sortkey', 'opts.sortdir'], function() { TopicFactory.query({ skip: ($scope.page - 1) * $scope.limit, limit: $scope.limit, - sort: $scope.opts.order + $scope.opts.sort + sort: ($scope.opts.sortdir ? '' : '-') + $scope.opts.sortkey }, function(data, headers) { $scope.topics = data; $scope.topicsTotal = headers("V-Total"); @@ -638,19 +639,19 @@ function($scope, $stateParams, $location, TopicFactory) { $scope.opts = { - sort: 'title', - order: '+' + sortkey: 'title', + sortdir: true }; $scope.page = Math.max($location.search().page || 1, 1); $scope.limit = 100; - $scope.$watchGroup(['page', 'opts.sort', 'opts.order'], function() { + $scope.$watchGroup(['page', 'opts.sortkey', 'opts.sortdir'], function() { TopicFactory.articles({ id: $stateParams.id, skip: ($scope.page - 1) * $scope.limit, limit: $scope.limit, - sort: $scope.opts.order + $scope.opts.sort + sort: ($scope.opts.sortdir ? '' : '-') + $scope.opts.sortkey }, function(data, headers) { $scope.articles = data; $scope.articlesTotal = headers("V-Total"); diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index 3e8c67c7..404d439d 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -203,12 +203,13 @@ app.directive('topicMenu', function() { return { scope: { - topic: '=' + topic: '=', + right: '@' }, restrict: 'E', - replace: true, templateUrl: 'html/directives/topic-menu.html', link: function($scope) { + $scope.dropdownRight = $scope.right === 'true'; $scope.renameTopic = function() { bootbox.prompt({ title: 'Rename topic', @@ -224,4 +225,15 @@ }; }); + app.directive('sortDir', function() { + return { + scope: { + ngModel: '=' + }, + restrict: 'E', + replace: true, + templateUrl: 'html/directives/sort-dir.html' + } + }); + })(); diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index 7d813899..8f1d3a1c 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -275,6 +275,19 @@ a:hover { margin-left: -18px; } } + .topic { + padding-left: 15px; + } + .popover-area { + position: absolute; + right: 0; + top: 0; + width: 250px; + height: 100%; + } + topic-menu { + position: absolute; + } } .colorbox { position: absolute; @@ -350,7 +363,11 @@ a:hover { } } -.inline-block { +.btn.btn-plain { + color: #333; +} + +topic-menu { display: inline-block; } 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 fbad9e2a..60eb7dc6 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 @@ -1,7 +1,5 @@ package de.vipra.util.model; -import java.io.File; -import java.io.IOException; import java.io.Serializable; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -22,7 +20,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.vipra.util.Constants; -import de.vipra.util.FileUtils; import de.vipra.util.MongoUtils; import de.vipra.util.NestedMap; import de.vipra.util.StringUtils; @@ -32,7 +29,7 @@ import de.vipra.util.an.QueryIgnore; @SuppressWarnings("serial") @Entity(value = "articles", noClassnameStored = true) @Indexes({ @Index("title"), @Index("date"), @Index("-created") }) -public class ArticleFull extends FileModel<ObjectId> implements Serializable { +public class ArticleFull implements Model<ObjectId>, Serializable { public static final Logger log = LoggerFactory.getLogger(ArticleFull.class); @@ -61,6 +58,10 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable { @QueryIgnore(multi = true) private List<SimilarArticle> similarArticles; + @Embedded + @QueryIgnore(all = true) + private List<ArticleWord> words; + @Embedded @QueryIgnore(multi = true) private ArticleStats stats; @@ -173,6 +174,14 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable { this.similarArticles = similarArticles; } + public List<ArticleWord> getWords() { + return words; + } + + public void setWords(final List<ArticleWord> words) { + this.words = words; + } + public ArticleStats getStats() { return stats; } @@ -211,18 +220,6 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable { meta.put(key, value); } - @Override - public void fromFile(final File file) throws IOException { - final List<String> lines = FileUtils.readFile(file); - setTitle(lines.get(0)); - setText(StringUtils.join(lines.subList(1, lines.size()))); - } - - @Override - public String toFileString() { - return getTitle() + "\n" + getText(); - } - @PrePersist public void prePersist() { modified = new Date(); diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java new file mode 100644 index 00000000..023d3879 --- /dev/null +++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java @@ -0,0 +1,48 @@ +package de.vipra.util.model; + +import java.io.Serializable; + +import org.mongodb.morphia.annotations.Embedded; + +@SuppressWarnings("serial") +@Embedded +public class ArticleWord implements Comparable<ArticleWord>, Serializable { + + private Word word; + + private Integer count; + + public ArticleWord() {} + + public ArticleWord(final String word, final int count) { + this.word = new Word(word); + this.count = count; + } + + public Word getWord() { + return word; + } + + public void setWord(final Word word) { + this.word = word; + } + + public Integer getCount() { + return count; + } + + public void setCount(final Integer count) { + this.count = count; + } + + @Override + public int compareTo(final ArticleWord o) { + return count.compareTo(o.getCount()); + } + + @Override + public String toString() { + return "ArticleWord [word=" + word + ", count=" + count + "]"; + } + +} diff --git a/vipra-util/src/main/java/de/vipra/util/model/FileModel.java b/vipra-util/src/main/java/de/vipra/util/model/FileModel.java deleted file mode 100644 index aa681bd6..00000000 --- a/vipra-util/src/main/java/de/vipra/util/model/FileModel.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.vipra.util.model; - -import java.io.File; -import java.io.IOException; - -import org.apache.commons.io.FileUtils; - -import de.vipra.util.Constants; - -@SuppressWarnings("serial") -public abstract class FileModel<IdType> implements Model<IdType> { - - public void writeToFile(final File file) throws IOException { - FileUtils.writeStringToFile(file, toFileString(), Constants.FILEBASE_ENCODING, false); - } - - public abstract void fromFile(File file) throws IOException; - - public abstract String toFileString(); - -} -- GitLab