package be.nikiroo.fanfix.library; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.UnknownHostException; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map.Entry; import be.nikiroo.fanfix.Instance; import be.nikiroo.fanfix.data.MetaData; import be.nikiroo.fanfix.data.Story; import be.nikiroo.fanfix.output.BasicOutput; import be.nikiroo.fanfix.output.BasicOutput.OutputType; import be.nikiroo.fanfix.supported.BasicSupport; import be.nikiroo.fanfix.supported.SupportType; import be.nikiroo.utils.Image; import be.nikiroo.utils.Progress; import be.nikiroo.utils.StringUtils; /** * Manage a library of Stories: import, export, list, modify. *
* Each {@link Story} object will be associated with a (local to the library) * unique ID, the LUID, which will be used to identify the {@link Story}. *
* Most of the {@link BasicLibrary} functions work on a partial (cover * MAY not be included) {@link MetaData} object. * * @author niki */ abstract public class BasicLibrary { /** * A {@link BasicLibrary} status. * * @author niki */ public enum Status { /** The library is ready. */ READY, /** The library is invalid (not correctly set up). */ INVALID, /** You are not allowed to access this library. */ UNAUTORIZED, /** The library is currently out of commission. */ UNAVAILABLE, } /** * Return a name for this library (the UI may display this). *
* Must not be NULL. * * @return the name, or an empty {@link String} if none */ public String getLibraryName() { return ""; } /** * The library status. * * @return the current status */ public Status getStatus() { return Status.READY; } /** * Retrieve the main {@link File} corresponding to the given {@link Story}, * which can be passed to an external reader or instance. *
* Do NOT alter this file. * * @param luid * the Library UID of the story * @param pg * the optional {@link Progress} * * @return the corresponding {@link Story} */ public abstract File getFile(String luid, Progress pg); /** * Return the cover image associated to this story. * * @param luid * the Library UID of the story * * @return the cover image */ public abstract Image getCover(String luid); /** * Return the cover image associated to this source. *
* By default, return the custom cover if any, and if not, return the cover
* of the first story with this source.
*
* @param source
* the source
*
* @return the cover image or NULL
*/
public Image getSourceCover(String source) {
Image custom = getCustomSourceCover(source);
if (custom != null) {
return custom;
}
List
* By default, return NULL.
*
* @param source
* the source to look for
*
* @return the custom cover or NULL if none
*/
public Image getCustomSourceCover(@SuppressWarnings("unused") String source) {
return null;
}
/**
* Fix the source cover to the given story cover.
*
* @param source
* the source to change
* @param luid
* the story LUID
*/
public abstract void setSourceCover(String source, String luid);
/**
* Return the list of stories (represented by their {@link MetaData}, which
* MAY not have the cover included).
*
* @param pg
* the optional {@link Progress}
*
* @return the list (can be empty but not NULL)
*/
protected abstract List
* All the cache can be deleted if NULL is passed as meta.
*
* @param luid
* the LUID of the {@link Story} to clear from the cache, or NULL
* for all stories
*/
protected abstract void deleteInfo(String luid);
/**
* Invalidate the {@link Story} cache (when the content has changed, but we
* already have it) with the new given meta.
*
* @param meta
* the {@link Story} to clear from the cache
*/
protected abstract void updateInfo(MetaData meta);
/**
* Return the next LUID that can be used.
*
* @return the next luid
*/
protected abstract int getNextId();
/**
* Delete the target {@link Story}.
*
* @param luid
* the LUID of the {@link Story}
*
* @throws IOException
* in case of I/O error or if the {@link Story} wa not found
*/
protected abstract void doDelete(String luid) throws IOException;
/**
* Actually save the story to the back-end.
*
* @param story
* the {@link Story} to save
* @param pg
* the optional {@link Progress}
*
* @return the saved {@link Story} (which may have changed, especially
* regarding the {@link MetaData})
*
* @throws IOException
* in case of I/O error
*/
protected abstract Story doSave(Story story, Progress pg)
throws IOException;
/**
* Refresh the {@link BasicLibrary}, that is, make sure all metas are
* loaded.
*
* @param pg
* the optional progress reporter
*/
public void refresh(Progress pg) {
getMetas(pg);
}
/**
* List all the known types (sources) of stories.
*
* @return the sources
*/
public synchronized List
* If the number of author is not too high, only one group with an empty
* name and all the authors will be returned.
*
* If not, the authors will be separated into groups:
*
* Cover images not included.
*
* @return the stories
*/
public synchronized List
* Cover images not included.
*
* @param type
* the type of story to retrieve, or NULL for all
*
* @return the stories
*/
public synchronized List
* Cover images not included.
*
* @param author
* the author of the stories to retrieve, or NULL for all
*
* @return the stories
*/
public synchronized List
* Will override any previous {@link Story} with the same LUID.
*
* @param story
* the {@link Story} to save
* @param luid
* the correct LUID or NULL to get the next free one
* @param pg
* the optional progress reporter
*
* @return the same {@link Story}, whose LUID may have changed
*
* @throws IOException
* in case of I/O error
*/
public synchronized Story save(Story story, String luid, Progress pg)
throws IOException {
Instance.getTraceHandler().trace(
this.getClass().getSimpleName() + ": saving story " + luid);
// Do not change the original metadata, but change the original story
MetaData meta = story.getMeta().clone();
story.setMeta(meta);
if (luid == null || luid.isEmpty()) {
meta.setLuid(String.format("%03d", getNextId()));
} else {
meta.setLuid(luid);
}
if (luid != null && getInfo(luid) != null) {
delete(luid);
}
story = doSave(story, pg);
updateInfo(story.getMeta());
Instance.getTraceHandler().trace(
this.getClass().getSimpleName() + ": story saved (" + luid
+ ")");
return story;
}
/**
* Delete the given {@link Story} from this {@link BasicLibrary}.
*
* @param luid
* the LUID of the target {@link Story}
*
* @throws IOException
* in case of I/O error
*/
public synchronized void delete(String luid) throws IOException {
Instance.getTraceHandler().trace(
this.getClass().getSimpleName() + ": deleting story " + luid);
doDelete(luid);
deleteInfo(luid);
Instance.getTraceHandler().trace(
this.getClass().getSimpleName() + ": story deleted (" + luid
+ ")");
}
/**
* Change the type (source) of the given {@link Story}.
*
* @param luid
* the {@link Story} LUID
* @param newSource
* the new source
* @param pg
* the optional progress reporter
*
* @throws IOException
* in case of I/O error or if the {@link Story} was not found
*/
public synchronized void changeSource(String luid, String newSource,
Progress pg) throws IOException {
MetaData meta = getInfo(luid);
if (meta == null) {
throw new IOException("Story not found: " + luid);
}
meta.setSource(newSource);
saveMeta(meta, pg);
}
/**
* Save back the current state of the {@link MetaData} (LUID MUST NOT
* change) for this {@link Story}.
*
* By default, delete the old {@link Story} then recreate a new
* {@link Story}.
*
* Note that this behaviour can lead to data loss.
*
* @param meta
* the new {@link MetaData} (LUID MUST NOT change)
* @param pg
* the optional {@link Progress}
*
* @throws IOException
* in case of I/O error or if the {@link Story} was not found
*/
protected synchronized void saveMeta(MetaData meta, Progress pg)
throws IOException {
if (pg == null) {
pg = new Progress();
}
Progress pgGet = new Progress();
Progress pgSet = new Progress();
pg.addProgress(pgGet, 50);
pg.addProgress(pgSet, 50);
Story story = getStory(meta.getLuid(), pgGet);
if (story == null) {
throw new IOException("Story not found: " + meta.getLuid());
}
delete(meta.getLuid());
story.setMeta(meta);
save(story, meta.getLuid(), pgSet);
pg.done();
}
}
*
* Note that the letters used in the groups can vary (except * and
* 0-9, which may only be present or not).
*
* @return the authors' names, grouped by letter(s)
*/
public List
*
*
* @param authors
* the full list of authors
* @param car
* the starting character, *, 0 or a capital
* letter
* @return the authors that fulfill the starting letter
*/
private List