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 acc0312b59c27e5c41eafccbcd882da3863570f7..e235dc29642ffc27070e858d8bef547e1e6daafb 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 69d76ea7fe4663f4c81b8ed1681b6aed83e7672c..c7f665a9d1833a0be7ce8b366701bba6b0c5c3cc 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 8e17e0e60defe916eb5cf689fbce5f0eb2b5b875..4cc9527af87226834080a2ea29b362e323592286 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 a47d1738d536aa37154dfde1e0580791eaa84fac..8911bcd2e62df83ebe84e51af5d279521034a94a 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 b37d4ad6e5686bd06fb3ea72eb98e7aca3bdb22c..681ebf3ec9d434de77843ff3c68c1d4bf19d0e2e 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 0000000000000000000000000000000000000000..155e996f9778a191c0a684d65903204d7ec4f0c6 --- /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 3423ff2bc20b5dde54324f3b2990914a202e29de..49ccb9e20545839284bb76e6f15aa5240fc6d337 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 8d3b33b220ada75728b5c6148f85f33ae72d1d63..f08e2a20b359357247851acfbbf4842f8e1c76f4 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 a0ea30eca7e9a3c47417785e58d1c2c8473c4574..f6d7e25a2e284c75d982789798e6e5cbd256fa96 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 c27bec67acd32dc01dc8fcb3335bf2fdb91c1df8..c7ecd073bd87f7782a620bbee998451e4b7a95ae 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 413d3d26e797e25071bd08c2984119ffe353af87..3a8babc5d093749827f395fb55930e6bbf233f0c 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 8012e8e96486f85cb64e07690c98b772ccd29db7..9c460c9da4dc6aba1b2666692516af8551276463 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 3e8c67c7fdb580d79c217cf825bc2a061f35c5bc..404d439da573128488eaddb27a4a026b2b613db7 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 7d81389997ea62e4ffc6ea90db9bee4aaee5fba8..8f1d3a1c11d2339f5dc6ab68201a1f07389e3498 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 fbad9e2a6cbc55b5d68b1a0f0c48b9c6562e9160..60eb7dc6cfaf1ea9f34a923f4078b5210cc4f119 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 0000000000000000000000000000000000000000..023d3879001d2a63a00115d2e8db27824858e1d7 --- /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 aa681bd69818e8f48062ba429ca3a5560b8db2a8..0000000000000000000000000000000000000000 --- 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(); - -}