Laureline's Wiki

Laureline's Wiki

Librarie libVCL

Librarie libVCL

La libVCL à été concue pour fournir un maximum d'abstractions pour la gestion des catalogues pour permettre une forte séparation avec l'implémentation ce qui permettrai éventuellement à d'autre implémentations que celle de base de fournir les même fonctionnalités pour, par exemple, des plateformes mobiles qui n'ont pas été prises en compte lors de ce projet. Cette séparation permet aussi la gestion des différents moteurs de stockage et emplacements de manière complétement transparente pour l'utilisateur.

Design de l'API

L'API publique à été concue pour offrir un maximum de fonctionnalités aux utilisateurs sans compromettre la flexibilité d'implémentation. La seule classe offrant une implémentation concrète étant la CatalogFactory chargée d'instancier les bonnes sous-classes en fonction du type de catalogue.

Si plus d'options de stockage venaient à se présenter (ex. JSON), un refactor de la factory serait très probablement nécéssaire afin de mieux supporter des implémentations arbitraires.

La première partie de l'API est une copie conforme du modèle de domaine en termes d'implémentation. Elle fournit un moyen de parcourir et modifier les données du catalogue de manière simple.

<uml> hide empty methods hide empty fields

class CatalogFactory «Singleton» {

+ load(url, config?): Catalog
+ create(url, config?): Catalog

} CatalogFactory ..> Catalog : creates

abstract class VolumeStore {

+ getVolume(metadata, type)
+ addVolume(metadata, volume)
+ deleteVolume(metadata)
+ editVolume(metadata)

} VolumeStore ..> Volume : loads VolumeStore <.. VolumeMetadata

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

} Catalog –> Category : root

interface CatalogNode {

+ name

} Catalog ← CatalogNode : catalog CatalogNode - VolumeMetadata

CatalogNode <|– Category interface Category { } CatalogNode “*” – “1” Category

CatalogNode <|– Series interface Series { } Series → SeriesMetadata : metadata

interface VolumeMetadata {

+ number
+ title

} VolumeMetadata ← Volume VolumeMetadata –> “0..1” SeriesMetadata

interface SeriesMetadata {

+ authors[*]
+ artists[*]
+ released
+ summary

} SeriesMetadata –> Tag : tags

interface Tag {

+ name

}

interface Volume { } Volume –> “*” Page

interface Page {

+ contents

}

</uml>

La seconde partie de l'API permet d'interragir avec les différents éléments de stockage liés aux données d'un catalogue.

  • CatalogConfig configure les chemins du cache du catalogue. Les différents chemins fournis par cette classe sont passés aux autres composants.
  • INodeFactory permet de créer des classes correspondant au moteur de stockage du catalogue.
  • ICatalogWriter permet de sauvegarder le catalogue en utilisant son moteur de stockage.
  • VolumeStore gère le chargement, sauvegarde, ajout, import et supression de volumes dans le système de fichiers. Cetaines de ces opérations peuvent être indisponnibles selon l'emplacement du catalogue.
  • ThumbnailStore permet de récupérer la miniature associée à un volume. Selon l'implémentation elle sera soit extraite du fichier cbz ou téléchargée et mise en cache.

<uml> hide empty methods hide empty fields

class CatalogFactory «Singleton» {

+ load(url, config?): Catalog
+ create(url, config?): Catalog

} CatalogFactory ..> Catalog : creates CatalogFactory –> CatalogConfig : defaultConfig

class CatalogConfig {

+ cachePath

}

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

} Catalog → CatalogConfig : config Catalog –> INodeFactory Catalog –> ICatalogWriter Catalog –> VolumeStore Catalog –> ThumbnailStore

interface INodeFactory {

+ createCategory(name): Category
+ createSeries(name): Series
+ createVolume(number, name?): VolumeMetadata
+ createSeriesMetadata(): SeriesMetadata
+ createTag(): Tag

}

interface ICatalogWriter {

+ write()
+ writeTo()

}

interface Volume { }

class VolumeFactory «Singleton» {

+ getLoader(type): IVolumeLoader
+ getWriter(): IVolumeWriter

}

VolumeFactory ..> IVolumeLoader : creates VolumeFactory ..> IVolumeWriter : creates

interface IVolumeLoader {

+ load(path, metadata): Volume

} IVolumeLoader ..> Volume : loads

interface IVolumeWriter {

+ load(path): Volume
+ createEmpty(): Volume
+ write(path, volume)
+ createPage(volume): Page
+ setPageName(page)
+ setPageContents(page)

}

abstract class VolumeStore {

+ getVolume(metadata, type): Future<Volume>
+ addVolume(metadata, volume)
+ deleteVolume(metadata)
+ editVolume(metadata): Future<Volume>

} VolumeStore …> IVolumeLoader : uses

abstract class ThumbnailStore {

+ getThumbnail(metadata): Future<byte[]>

} </uml>

 

Moteurs de Stockage

La librarie implémente actuellement deux moteurs de stockage: XML et SQLite. Ces deux moteurs se conforment à l'API définie en implémentant les interfaces fournies.

Moteur XML

Le moteur de stockage XML permet de stocker les métadonnées sous la forme d'un ou plusieurs fichiers XML. Les fichiers de ce moteur peuvent être modifiés à la main par un utilisateur expérimenté. Le moteur de stockage XML se démarque par sa simplicité en terme d'implémentation.

L'avantage de ce moteur de stockage est qu'il peut être utilisé pour aussi bien des catalogues locaux que distants. Il est même possible de simplement transférer un catalogue XML d'un emplacement local sur un serveur HTTP et de le rendre disponnible comme catalogue distant.

Le catalogue est entièrement chargé en mémoire lors de sa lecture depuis le disque. Le parsing s'effectue à l'aide de l'API DOM fournie par java en utilisant principalement des requètes XPath pour facilement trier les défférent types de noeuds.

L'écriture s'effectue à l'aide de la librairie dom4j car l'API DOM java est difficile d'utilisation. Lors de son écriture, un catalogue est complétement ré-écrit sur le disque.

Un exemple de catalogue XML peut être trouvé sur le CD-ROM.

Moteur SQLite

Le moteur SQLite permet d'enregistrer des informations sur les catalogues sur une base de données SQLite.

Il se base sur le framework OrmLite. Voici son modèle UML:

<uml> hide empty fields hide empty methods

class category {

+ id: int {id}
+ parent_id: int {fk}
+ name: string

}

class series {

+ id: int {id}
+ parent_id: int {fk}
+ metadata_id: int {fk}
+ name: string

}

class series_metadata {

+ id: int {id}
+ summary: string
+ released: int
+ author: string
+ artist: string
+ tag: string

}

class volume_metadata {

+ id: int {id}
+ series_parent_id: int {fk}
+ category_parent_id: int {fk}
+ number: int
+ name: string

}

category – “*” category category – “*” series

series → series_metadata

category – “*” volume_metadata series – “*” volume_metadata </uml>

Malheureusement, l'utilisation d'OrmLite n'a pas apporté les résultats souhaités car les objets actifs liés à la base de données fonctionnent de manière particulière, ce qui est incompatible avec le fonctionnement de la libVCL.

Une solution possible est de créer, depuis nos objets actifs SQLite, des objets contenant les informations mais sans liaison à la base de données. Notre application pourrait alors travailler dessus en faisant abstraction de ces problèmes. Puis, lors de la sauvegarde, notre moteur n'aurait plus qu'à parcourir ces objets et les enregistrer.

Cette solution n'a pas été utilisée pour ce projet car elle était jugée plus compliquée que de travailler sur des objets actifs. Cependant, il s'est avéré, après avoir résolu les problèmes liés aux objets actifs, qu'elle était en réalité bien plus simple. Voici ci-dessous une description des problèmes rencontrés et la solution apportée.

Tout d'abord, il est impossible de créer des relations entre des objets qui ne sont pas enregistrés dans la base de données. Par exemple, si on crée un volume qui doit appartenir à une série et que celle-ci n'a pas encore été enregistrée dans la base de données, cela va lever une exception. La solution est qu'à chaque création d'objets, il faut immédiatement l'enregistrer dans la base de données. Ce qui a pour désavantage qu'il n'est pas possible de travailler sur des objets temporaires n'étant pas enregistrés.

Ensuite, une catégorie doit pouvoir contenir à la fois des séries et d'autres catégories. Cependant depuis SQLite, cela se traduit par deux collections distinctes, alors que notre API veut que nous puissions récupérer d'une fois la liste contenant les deux et de pouvoir ajouter un élément depuis celle-ci sans savoir lequel. Il a donc été nécessaire de créer un objet qui émule une liste contenant les deux listes et qui, à l'ajout d'un élément, récupère le type de celui-ci et l'ajoute à la bonne liste.

Enfin, lorsque l'on modifie des informations sur un enregistrement, il faut appeler une méthode qui met à jour cet élément sur la base de données et l'enregistrement lui-même. Le problème vient de cette dernière opération car quand OrmLite met à jour un élément il change les références vers les objets parents, ce qui a pour conséquence de corrompre la hiérarchie du catalogue.

Le dernier problème n'a pas été résolu car le seul moyen de le résoudre est d'implémenter une autre solution qui ne travaille pas avec des objets actifs. Ce qui reviendrait à recommencer le développement du début, alors que ce problème a été découvert à la fin du projet, après de longues heures de recherche.

Il en résulte que les catalogues SQLite ne sont accessibles qu'en lecture et qu'il faut redémarrer l'application à chaque modification du catalogue.

 

Gestion des Catalogues

Il existe deux types de catalogues: Locaux et Distants. Un catalogue local est disponible en écriture ET en lecture tandis qu'un catalogue distant n'est disponible qu'en lecture. Un catalogue est définit par son URL de base. Deux catalogues différents ne devraient pas tenter de partager une même racine en écriture.

<uml> hide empty members

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

}

Catalog <|– LocalCatalog Catalog <|– RemoteCatalog

LocalCatalog –> LocalVolumeStore RemoteCatalog –> RemoteVolumeStore

LocalCatalog –> LocalThumbnailStore RemoteCatalog –> RemoteThumbnailStore </uml>

Un catalogue local stocke toute ses informations dans sa racine. Le dossier dans lequel se trouve le fichier racine du catalogue détermine l'emplacement de stockage des volumes de ce catalogue. Etant donné que tous les chemins référencés dans un catalogue sont relatifs à son dossier de base, il est possible de déplacer un catalogue sur le système de fichiers sans que les métadonnées ne soient corrompues.

Un catalogue distant est très semblable à un catalogue local, excepté qu'il n'est disponible qu'en lecture seule pour ses utilisateurs. Il est d'ailleurs possible de copier les fichiers d'un catalogue local au format XML sur un point d'accès HTTP et il sera alors disponible en tant que catalogue distant, il sera néanmoins nécessaire de générer les miniatures pour qu'elles soient disponibles.

 

Gestion des Volumes

La gestion des volumes utilise l'api 7zip-JavaBindings. Cette api permet d'interragir avec 7zip, logiciel gratuit, permettant de décompresser et compresser beaucoup de format d'archives actuels, tel que rar, 7zip, zip, bzip2, etc. 7zip est par ailleurs disponible sur tous les Systèmes d'exploitations courants. 7zip-JavaBindings est aussi disponible sur les systèmes d'exploitation courants (windows, linux, mac osx et arm).

Ce choix a été motivé par l'existance même de cette api. En effet, le fait de devoir potentiellement traiter plusieurs types d'archives aurait été compliqué à implémenter en java et pas nécessairement supporté par java de base.

C'était un choix risqué à prendre, car personne dans le groupe n'avait utilisé 7zip-JavaBindings auparavant. Nous ne savions donc pas comment l'api allait réagir, si elle avait des bugs ou pas, etc.

7zip-JavaBindings fonctionne en mode 1-1, c'est à dire que lors de l'extraction d'une archive, l'on doit appeler systématiquement la fonction extractSlow() qui permet d'extraire un seul fichier de l'archive à la fois. Cette fonction est lente, d'où son nom, mais comparé à l'autre solution, qui se comporte comme une approche c++, cette solution parraissait la plus simple. Après quelques tests il s'est avéré que cette fonction n'est pas si lente que cela. A cause de ce comportement de 7zip-Javabindings, chaque archive doit être extraite via une boucle, qui permet de traverser l'entier des fichiers présents dans l'archive.

Afin d'éviter de trop surcharger l'application, qui peut tourner sur un pc plus lent que les nôtres, un système de Lazy loading a été mis en place afin de ne pouvoir charger qu'une seule partie d'un volume et non l'entier. Ce système prend en paramètre une certaine plage de page à charger et permet de charger ces dernières uniquement. Au fur et à mesure de l'avancement dans les pages, les suivantes peuvent être chargées dynamiquement.

L'architecture des volumes se présente comme suit:

<uml> hide empty fields hide empty methods

Interface Volume Volume : getPages(): List<Page> Volume : getMetadata(): VolumeMetadata

Volume <|– LazyVolume LazyVolume : loadPage(int index) LazyVolume : loadPages(int start, int end)

Volume <|– EagerVolume

Volume <|– MutableVolume MutableVolume : createPage(): Page

Volume *- Page :contains

Interface Page Page : getImage(): byte[] Page : setPath() Page : compareTo(Page other): int

Page <|– LazyPage Page <|– EagerPage Page <|– MutablePage MutablePage : getFilename(): String MutablePage : setImage(byte[] i) MutablePage : setFilename(String name)

MutableVolume <.. VolumeWriter :creates

EagerVolume <.. EagerVolumeLoader :creates LazyVolume <.. LazyVolumeLoader :creates

Interface IVolumeLoader IVolumeLoader : load(String path, VolumeMetadata metadata): Volume EagerVolumeLoader –|> IVolumeLoader LazyVolumeLoader –|> IVolumeLoader

Class ImageReader ImageReader : write(byte[] data): int ImageReader : makePage(): Page LazyVolumeLoader …> ImageReader :uses EagerVolumeLoader …> ImageReader :uses

Class “7zip-JavaBindings” as 7jb ImageReader ..> 7jb :uses VolumeWriter …> 7jb :uses LazyVolumeLoader …> 7jb :uses EagerVolumeLoader …> 7jb :uses

Interface IVolumeWriter IVolumeWriter : load(String path): Volume IVolumeWriter : write(String path, Volume volume) IVolumeWriter : createEmpty(): Volume IVolumeWriter : createPage(Volume volume): Page IVolumeWriter : setPageName(Page page, String name) IVolumeWriter : setPageContents(Page page, byte[] content) VolumeWriter –|> IVolumeWriter

</uml>

Comme on peut le voir sur le schéma ci-dessus, les volumes sont composés de pages. Ces dernières sont de type différents selon les différents types de volumes. Un EagerVolume contiendra des EagerPage, un LazyVolume des LazyPage et ainsi de suite. Chaque type de page est légèrement différent d'un autre, car ils ne servent pas les mêmes usages.

Nous pouvons aussi observer que les deux VolumeLoader (Eager et Lazy) utilisent tous-deux la classe ImageReader. Cette dernière fait le lien entre les 7zip-JavaBindings et la librairie. Cette classe est particulièrement utilisée pour récupérer les données de l'archive ouverte. Elle contient notamment la fonction write(byte[] data) qui permet à 7zip d'aller écrire dans ce byte array ce qu'il a lu dans le fichier en extraction (une archive peut évidement contenir plusieurs fichiers). Lors de l'écriture c'est un peu plus compliqué. Le VolumeWriter appèle une fonction 7zip-JavaBindings, qui elle même va appeler une fonction de l'api 7zip. Cette fonction 7zip va ensuite rappeler via un callback la classe StatusCallback (classe privée de VolumeWriter), qui contient les fonctions getStream(int index) et getItemInformation(int index, OutItemFactory<IOutItemZip> outItemFactory). Ces dernières permettent à 7zip de créer le fichier dans l'archive, avec les bonnes permissions, bon chemin, etc.

Petit bémol, en travaillant directement avec l'archive, les ficheirs sont lus dans leur ordre au sein de l'archive. Ceci ne garantit pas du tout le bon ordre des pages d'un volume. En effet si la page 2 est enregistrée avant la 0 et la 1, alors le premier fichier lu sera la page 2 et non la page 0 comme on pourrait s'y attendre. Ceci a dû être traité lors du chargement du volume. Le EagerVolumeLoader chargeant toutes les pages d'un coup, il n'a pas été difficile de trier ses pages une fois chargées en mémoire. Le LazyVolumeLoader permettant de ne charger qu'un certain nombre de pages, ne permet pas de trier aussi facilement les pages. En effet, il suffit que la page 0 soit la dernière, alors les 10 premières pages peuvent être les pages 1 à 10, mais devraient être les pages 0 à 9. Une solution simple serait de charger, trier et réécrire le volume, mais c'est très peu propre et non-performant. Ceci posant un sérieux problème lors du chargement du volume, le chargement en mode Lazy n'a pas été implémenté dans la liseuse (qui devrait dans ce mode demander les pages suivantes à chaque changement de page affichée).

 

Crawler

Le crawler est un module supplémentaire fournit par la librairie qui permet de télécharger une série à partir d'un site web. Ceci permet la lecture “on the go” de séries sans la nécessité de posséder une connexion internet.

Le fonctionnement et la structure du crawler est une ré-implémentation du projet MangaArchive qui a inspiré la création de cette application.

<uml> hide empty fields hide empty methods

class CrawlerFactory {

+ register(crawler)
+ registerAll()
+ forUrl(url): Optional<ICrawler>
+ getExecutor(): Executor
+ setExecutor(executor)

} CrawlerFactory ..> ICrawler : provides

interface ICrawler {

+ canCrawl(url): boolean
+ crawl(url): WebSeries
+ crawlAsync(url): ProgressFuture<WebSeries>

} ICrawler .> WebSeries : creates

class Builder {

+ build(webSeries): boolean
+ buildAsync(webSries): ProgressFuture<boolean>

} WebSeries .> Builder

WebSeries –> WebVolume WebVolume –> WebChapter WebChapter –> WebPage

</uml>

Le module fonctionne en deux temps: Crawling et Building. La phase de crawling construit un descripteur à partir des informations disponibles sur la page de la série. Un descripteur contient les métadonnées de la série construite en “best effort” et la référence vers toutes les images (pages) nécessaires à la construction de la série. Les images sont organisées en chapitres eux mêmes organisés en volumes. L'inclusion des chapitres dans la structure permet à certaines implémentations de mieux gérer la récupération des URLs des pages. Une fois la création du descripteur complet il est possible de le sauvegarder au format JSON pour une utilisation future, par exemple si le processus de construiction est interrompu. La phase de building utilise le descripteur créé lors de la phase précédente, effectue le téléchargement de toutes les images et les ajoute à un nouveau volume qui est alors ajouté à la série. Une fois le processus terminé, le série est ajoutée à la racine du catalogue et peut être modifiée par l'utilisateur.

Chacun des deux modules fournit deux modes de fonctionnement: Synchrone et Asynchrone. Le mode synchrone fontionne en appelant le mode Asynchrone et en bloquant sur le résultat. Le mode asynchrone détache son fonctionnement en arrière-plan dans l'executor fournit par la CrawlerFactory et retourne un ProgressFuture. L'objet ProgressFuture hérite de CompletableFuture et permet en plus de reporter l'avancement de la tâche aux objets s'étant enregistrés pour recevoir les notifications de progrès.

Gestion de Progression

La gestion de la progression se fait au travers d'une classe héritant de CompletableFuture. Les Promesses et Futures sont déjà fortement utilisées dans d'autre parties de l'application ou les opérations asynchrones sont nécessaires, par exemple, lors du chargement d'un volume. Dans le cas courant, en plus de fournir la promesse d'une réponse, étant donné que l'opération peut durer plusieurs minutes, elle doit aussi fournir à son appelant un moyen de surveiller l'avancement de l'opération. Une option aurait été de créer une classe spécialisée pour cette tâche, mais cela aurait nécessité à l'appelant de devoir gérer un objet supplémentaire. L'option choisie à été de créer une classe ProgressFuture qui dérive de CompletableFuture et qui fournit la possibilité à la tâche de communiquer son avancement à travers un canal asynchrone à son appelant.

<uml> hide empty fields hide empty methods

class CompletableFuture<TValue> {

+ whenComplete(consumer)
+ whenCompleteAsync(consumer, executor)

}

CompletableFuture <|– ProgressFuture class ProgressFuture<TValue> {

+ whenProgress(consumer)
+ whenProgressAsync(consumer, executor)
+ whenMessage(consumer)
+ whenMessageAsync(consumer, executor)

}

</uml>