--- /dev/null
+package be.nikiroo.fanfix.searchable;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import org.jsoup.helper.DataUtil;
+import org.jsoup.nodes.Document;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.supported.BasicSupport;
+import be.nikiroo.fanfix.supported.SupportType;
+
+/**
+ * This class supports browsing through stories on the supported websites. It
+ * will fetch some {@link MetaData} that satisfy a search query or some tags if
+ * supported.
+ *
+ * @author niki
+ */
+public abstract class BasicSearchable {
+ private SupportType type;
+ private BasicSupport support;
+
+ /**
+ * Create a new {@link BasicSearchable} of the given type.
+ *
+ * @param type
+ * the type, must not be NULL
+ */
+ public BasicSearchable(SupportType type) {
+ setType(type);
+ support = BasicSupport.getSupport(getType(), null);
+ }
+
+ /**
+ * The support type.
+ *
+ * @return the type
+ */
+ public SupportType getType() {
+ return type;
+ }
+
+ /**
+ * The support type.
+ *
+ * @param type
+ * the new type
+ */
+ protected void setType(SupportType type) {
+ this.type = type;
+ }
+
+ /**
+ * The associated {@link BasicSupport}.
+ * <p>
+ * Mostly used to download content.
+ *
+ * @return the support
+ */
+ protected BasicSupport getSupport() {
+ return support;
+ }
+
+ /**
+ * Get a list of tags that can be browsed here.
+ *
+ * @return the list of tags
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ abstract public List<SearchableTag> getTags() throws IOException;
+
+ /**
+ * Fill the tag (set it 'complete') with more information from the support.
+ *
+ * @param tag
+ * the tag to fill
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ abstract protected void fillTag(SearchableTag tag) throws IOException;
+
+ /**
+ * Search for the given term and return a list of stories satisfying this
+ * search term.
+ * <p>
+ * Not that the returned stories will <b>NOT</b> be complete, but will only
+ * contain enough information to present them to the user and retrieve them.
+ * <p>
+ * URL is guaranteed to be usable, LUID will always be NULL.
+ *
+ * @param search
+ * the term to search for
+ *
+ * @return a list of stories that satisfy that search term
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ abstract public List<MetaData> search(String search) throws IOException;
+
+ /**
+ * Search for the given tag and return a list of stories satisfying this
+ * tag.
+ * <p>
+ * Not that the returned stories will <b>NOT</b> be complete, but will only
+ * contain enough information to present them to the user and retrieve them.
+ * <p>
+ * URL is guaranteed to be usable, LUID will always be NULL.
+ *
+ * @param tagId
+ * the tag to search for
+ *
+ * @return a list of stories that satisfy that search term
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ abstract public List<MetaData> search(SearchableTag tag) throws IOException;
+
+ /**
+ * Load a document from its url.
+ *
+ * @param url
+ * the URL to load
+ * @return the document
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ protected Document load(String url) throws IOException {
+ return load(new URL(url));
+ }
+
+ /**
+ * Load a document from its url.
+ *
+ * @param url
+ * the URL to load
+ * @return the document
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ protected Document load(URL url) throws IOException {
+ return DataUtil.load(Instance.getCache().open(url, support, false),
+ "UTF-8", url.toString());
+ }
+
+ /**
+ * Return a {@link BasicSearchable} implementation supporting the given
+ * type, or NULL if it does not exist.
+ *
+ * @param type
+ * the type, must not be NULL
+ *
+ * @return an implementation that supports it, or NULL
+ */
+ public static BasicSearchable getSearchable(SupportType type) {
+ BasicSearchable support = null;
+
+ switch (type) {
+ case FIMFICTION:
+ // TODO
+ break;
+ case FANFICTION:
+ support = new Fanfiction(type);
+ break;
+ case MANGAFOX:
+ // TODO
+ break;
+ case E621:
+ // TODO
+ break;
+ case YIFFSTAR:
+ // TODO
+ break;
+ case E_HENTAI:
+ // TODO
+ break;
+ case MANGA_LEL:
+ // TODO
+ break;
+ case CBZ:
+ case HTML:
+ case INFO_TEXT:
+ case TEXT:
+ case EPUB:
+ break;
+ }
+
+ return support;
+ }
+}
--- /dev/null
+package be.nikiroo.fanfix.searchable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a tag that can be searched on a supported website.
+ *
+ * @author niki
+ */
+public class SearchableTag {
+ private String id;
+ private String name;
+ private boolean complete;
+ private long count;
+ private List<SearchableTag> children;
+
+ /**
+ * Create a new {@link SearchableTag}.
+ *
+ * @param id
+ * the ID (usually a way to find the linked stories later on)
+ * @param name
+ * the tag name, which can be displayed to the user
+ * @param complete
+ * TRUE for a {@link SearchableTag} that cannot be "filled" by
+ * the {@link BasicSearchable} in order to get (more?) subtag
+ * children
+ */
+ public SearchableTag(String id, String name, boolean complete) {
+ this.id = id;
+ this.name = name;
+ this.complete = complete;
+
+ children = new ArrayList<SearchableTag>();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * This tag can still be completed via a "fill" tag operation from a
+ * {@link BasicSearchable}, in order to gain (more?) subtag children.
+ *
+ * @return TRUE if it can
+ */
+ public boolean isComplete() {
+ return complete;
+ }
+
+ /**
+ * This tag can still be completed via a "fill" tag operation from a
+ * {@link BasicSearchable}, in order to gain (more?) subtag children.
+ *
+ * @param complete
+ * TRUE if it can
+ */
+ public void setComplete(boolean complete) {
+ this.complete = complete;
+ }
+
+ /**
+ * The number of items that can be found with this tag if it is searched.
+ * <p>
+ * Will report the number of subtags by default.
+ *
+ * @return the number of items
+ */
+ public long getCount() {
+ long count = this.count;
+ if (count <= 0) {
+ count = children.size();
+ }
+
+ return count;
+ }
+
+ /**
+ * The number of items that can be found with this tag if it is searched,
+ * displayable format.
+ * <p>
+ * Will report the number of subtags by default.
+ *
+ * @return the number of items
+ */
+ public String getCountDisplay() {
+ long count = this.count;
+ if (count <= 0) {
+ count = children.size();
+ }
+
+ if (count > 999999) {
+ return count / 1000000 + "M";
+ }
+
+ if (count > 2000) {
+ return count / 1000 + "k";
+ }
+
+ return Long.toString(count);
+ }
+
+ /**
+ * The number of items that can be found with this tag if it is searched.
+ *
+ * @param count
+ * the new count
+ */
+ public void setCount(long count) {
+ this.count = count;
+ }
+
+ /**
+ * The subtag children of this {@link SearchableTag}.
+ * <p>
+ * Never NULL.
+ * <p>
+ * Note that if {@link SearchableTag#isComplete()} returns false, you can
+ * still fill (more?) subtag children with a {@link BasicSearchable}.
+ *
+ * @return the subtag children, never NULL
+ */
+ public List<SearchableTag> getChildren() {
+ return children;
+ }
+
+ /**
+ * Add the given {@link SearchableTag} as a subtag child.
+ *
+ * @param tag
+ * the tag to add
+ */
+ public void add(SearchableTag tag) {
+ children.add(tag);
+ }
+
+ /**
+ * Display a DEBUG {@link String} representation of this object.
+ */
+ @Override
+ public String toString() {
+ String rep = name + " [" + id + "]";
+ if (!complete) {
+ rep += "*";
+ }
+
+ if (getCount() > 0) {
+ rep += " (" + getCountDisplay() + ")";
+ }
+
+ if (!children.isEmpty()) {
+ String tags = "";
+ int i = 1;
+ for (SearchableTag tag : children) {
+ if (!tags.isEmpty()) {
+ tags += ", ";
+ }
+
+ if (i > 10) {
+ tags += "...";
+ break;
+ }
+
+ tags += tag;
+ i++;
+ }
+
+ rep += ": " + tags;
+ }
+
+ return rep;
+ }
+}