OpenXML et Java: manipuler la structure d’un document OpenXML
Prérequis pour réaliser et exécuter les exemples de cet article : avoir installé le JDK 5 de Java SE (TM Copyright Sun Microsystems) et l’IDE Eclipse 3.1 (TM Copyright Sun Microsystems) au minimum, avoir des connaissances en Java.
Remarque importante : le code source associé à cet article est destiné à des fins de démonstration uniquement, nous vous déconseillons fortement de l’utiliser dans des projets industriels. Les exemples sont basés sur les spécifications 1.3 du format Office OpenXML, cependant bien que ceux-ci doivent être compatibles avec la version 1.4, il se peut que vous rencontriez des problèmes, dans ce cas merci de me contacter à julien@chable.net.
Téléchargements : le code source associé
.gif)
Sur cette page
Le paquet
Un paquet OpenXML se présente sous la forme d’une archive Zip contenant un ensemble de fichiers compressés, chaque fichier est une partie du paquet.
Pour lister les parties d’un paquet OpenXML, nous allons utiliser le package java.util.zip de Java. Le code ci-après procède de la façon suivante : il ouvre le fichier Zip puis liste toutes les entrées, pour chaque entrée on transforme son nom en URI (Uniform Resource Identifiers - requis par les spécifications OpenXML) qui sera ensuite ajouté à la liste retournée par la méthode :
public static ArrayList<URI>
listPackageParts(ZipFile zipfile){
ArrayList<URI>
retArr = new ArrayList<URI>
();
Enumeration entries = zipfile.entries();
// On parcourt toutes les entrées du fichier ZIP
while (entries.hasMoreElements()){
ZipEntry entry = (ZipEntry)entries.nextElement();
// On transforme le nom de l'entrée en URI
URI entryURI = null;
try {
entryURI = new URI(entry.getName());
} catch (URISyntaxException e) {
continue;
}
// On ajoute l'URI de l'entrée à la liste des noms des parties
retArr.add(entryURI);
}
return retArr;
}
Listing 1 (classe Package – fichier Package.java)
L’exemple ci-dessous utilise le code du listing 1 pour afficher dans la console toutes les parties du package :
...
final String APP_ROOT = System.getProperty("user.dir") + File.separator;
ZipFile zipFile = null;
try {
zipFile = new ZipFile(APP_ROOT + "sample.docx");
} catch (IOException e) {
...
}
// On affiche tous les noms des parties du package
for (URI uri : Package.listPackageParts(zipFile))
System.out.println(uri.getPath());
...
Listing 2
Une seconde version du listing 2 utilisant la classe Package du mini framework associé à cet article :
...
Package p = Package.open(zipFile, PackageAccess.Read);
for (PackagePart part : p.getParts()) {
System.out.println(part.getUri());
}
Listing 3
Exécutez ces exemples, un résultat similaire s’affiche :
_rels/.rels
docProps/app.xml
docProps/core.xml
word/_rels/document.xml.rels
word/document.xml
word/endnotes.xml
word/fontTable.xml
word/footnotes.xml
word/media/image1.png
word/settings.xml
word/styles.xml
word/theme/theme1.xml
word/webSettings.xml
Félicitations, maintenant vous savez lister les parties d’un document OpenXML, mais ne vous réjouissez pas trop vite, la liste des parties ci-dessus n’est pas tout à fait complète ! En effet il manque un fichier important et obligatoire à la structure d’un paquet OpenXML : le fichier des types de contenu nommé [Content_Types].xml. Ce fichier est traité juste après par la prochaine partie.
Remarque : Le code précédent n’a pas listé le fichier [Content_Types].xml car il ne possède pas un nom valide pour un URI. Au niveau du code Java, cette invalidité a lancé l’exception URISyntaxException et indiqué à la boucle de continuer sans ajouter ce fichier à la liste des parties.
Voici la hiérarchie complète ainsi qu’un aperçu du fichier d’exemple que nous utilisons pour nos exemples :
.jpg)
.jpg)
Haut de pageLes parties et le type de contenu
Une partie possède plusieurs propriétés qu’il est important de bien connaître :
| • | un URI : sa location dans le paquet |
| • | un identifiant unique : utilisé par les références dans les documents |
| • | un type de contenu : le fameux content-type MIME. |
C’est la dernière propriété que le fichier [Content_Types].xml renseigne, c'est-à-dire l’information qui permet de savoir quelle est la véritable nature d’une partie : est-ce un contenu XML ? est-ce une image ? l’image est-elle de type jpeg ou png ? ...
Voici un exemple de fichier [Content_Types].xml :
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<Default Extension="xml" ContentType="application/xml" />
<Default Extension="png" ContentType="image/png" />
<Override PartName="/word/document.xml"
ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" />
<Override PartName="/docProps/app.xml"
ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" />
<Override PartName="/docProps/core.xml"
ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
...
</Types>
Nous constatons qu’il existe deux éléments de type différent : default et override. Le type default possède un attribut nommé Extension et un autre ContentType. Ce type indique que tous les fichiers possédant une extension égale à la valeur de l’attribut Extension sont associés au type de contenu renseigné par l’attribut ContentType. Le type override permet de spécifier un type de contenu différent de celui spécifié par le type par défaut. Par exemple dans le fichier ci-dessus, la partie /docProps/core.xml est un fichier dont l’extension est .xml, mais dont le type de contenu le définit comme un fichier de propriétés et non uniquement comme un simple fichier XML. Le type override comprend donc naturellement deux propriétés, l’attribut PartName qui spécifie l’URI de la partie concernée, et l’attribut ContentType qui définit le type de contenu spécifique de cette partie.
Reprenons l’exemple du listing 3 et rajoutons, en plus de l’URI, le type de contenu de la partie :
Package p = Package.open(zipFile, PackageAccess.Read);
for (PackagePart part : p.getParts()) {
System.out.println(part.getUri() + " -> " + part.getContentType());
}
Listing 4
Le résultat affiche à présent l’URI des parties ainsi que le type de contenu associé :
docProps/core.xml -> application/vnd.openxmlformats-package.core-properties+xml
word/_rels/document.xml.rels -> application/vnd.openxmlformats-package.relationships+xml
word/document.xml -> application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml
word/media/image1.png -> image/png
...
Récupérer le type de contenu d’une partie avec la classe Package semble simple, néanmoins, voyons ce qui se passe réellement dans les coulisses, à savoir comment cela a été implémenté dans la classe ContentTypeHelper.
Tout d’abord, nous récupérons l’entrée Zip du fichier [Content_Types].xml à l’aide de la méthode getContentTypeZipEntry. :
public static final String CONTENT_TYPES_FILENAME = "[Content_Types].xml";
...
private ZipEntry getContentTypeZipEntry() {
// On énumère toutes les entrées de l'archive Zip jusqu'à trouver
// celle du fichier [Content_Types].xml
Enumeration entries = zipArchive.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
if (entry.getName().equals(CONTENT_TYPES_FILENAME))
return entry;
}
return null;
}
Listing 5 (classe Package.ContentTypeHelper – fichier Package.java)
Une fois l’entrée Zip du fichier récupérée, nous obtenons le flux de lecture qui sera utilisé par le parser DOM pour décomposer le contenu XML du fichier en un arbre à partir duquel nous extrairons les valeurs des différents types de contenu. Ces valeurs seront placées dans deux collections distinctes représentant les deux types de contenu différents – Default et Override - composant un fichier de types de contenu :
private static final String TYPES_TAG_NAME = "Types";
private static final String DEFAULT_TAG_NAME = "Default";
private static final String EXTENSION_ATTRIBUTE_NAME = "Extension";
private static final String CONTENT_TYPE_ATTRIBUTE_NAME = "ContentType";
private static final String OVERRIDE_TAG_NAME = "Override";
private static final String PART_NAME_ATTRIBUTE_NAME = "PartName";
...
// On récupère l’entrée Zip (
contentTypeZipEntry = getContentTypeZipEntry();
// Récupération du flux d’entrée
InputStream inStream = null;
try {
inStream = zipArchive.getInputStream(contentTypeZipEntry);
} catch (IOException e) {
...
}
// Création du parser DOM
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
documentBuilderFactory.setIgnoringElementContentWhitespace(true);
DocumentBuilder documentBuilder;
try {
documentBuilder = documentBuilderFactory.newDocumentBuilder();
// On parse le document XML en arbre DOM
Document xmlContentTypetDoc = documentBuilder.parse(inStream);
// On parcourt les types par défaut
NodeList defaultTypes = xmlContentTypetDoc
.getElementsByTagName(DEFAULT_TAG_NAME);
for (int i = 0; i < defaultTypes.getLength(); ++i="") {
// On ajoute l'élément du type par défaut dans sa collection
Node type = defaultTypes.item(i);
String extension = type.getAttributes().getNamedItem(
EXTENSION_ATTRIBUTE_NAME).getNodeValue();
String contentType = type.getAttributes().getNamedItem(
CONTENT_TYPE_ATTRIBUTE_NAME).getNodeValue();
// Ajout du type à la collection des types par défaut
addDefaultContentType(extension, contentType);
}
// On parcourt les types surdéfinis
NodeList overrideTypes = xmlContentTypetDoc
.getElementsByTagName(OVERRIDE_TAG_NAME);
for (int i = 0; i < overrideTypes.getLength(); ++i="") {
// On ajoute l'élément du type par défaut dans sa collection
Node type = overrideTypes.item(i);
URI uri = new URI(type.getAttributes().getNamedItem(
PART_NAME_ATTRIBUTE_NAME).getNodeValue());
String contentType = type.getAttributes().getNamedItem(
CONTENT_TYPE_ATTRIBUTE_NAME).getNodeValue();
// Ajout du type à la collection des types surdéfinis
addOverrideContentType(uri, contentType);
}
}
...
private void addDefaultContentType(String extension, String contentType) {
defaultContentType.put(extension, contentType);
}
private void addOverrideContentType(URI partUri, String contentType) {
if (overrideContentType == null)
overrideContentType = new TreeMap<URI, String="">
();
overrideContentType.put(partUri.normalize(), contentType);
}
Listing 5-1 (classe Package.ContentTypeHelper – fichier Package.java)
Une fois que les collections de types de contenu sont créées, il devient alors simple d’implémenter une fonction permettant de retrouver rapidement le type de contenu d’une partie :
public static final char FORWARD_SLASH_CHAR = ‘/’;
...
public String getContentType(URI partUri) {
URI normPartUri;
try {
normPartUri = new URI(PackageURIHelper.FORWARD_SLASH_CHAR
+ partUri.normalize().getPath());
}
...
if ((this.overrideContentType != null)
&& this.overrideContentType.containsKey(normPartUri))
return this.overrideContentType.get(normPartUri);
String extension = PackageURIHelper.getExtensionForPartURI(partUri);
if (this.defaultContentType.containsKey(extension))
return this.defaultContentType.get(extension);
return null;
}
Listing 5-2 (classe Package.ContentTypeHelper – fichier Package.java)
Une fois la nature d’une partie connue, le développeur possède toutes les informations nécessaires pour pouvoir extraire cette partie et interpréter son contenu.
Dans un premier temps, nous récupérons le flux de lecture de l’entrée Zip de la partie :
/// Classe PackagePart
public InputStream getInputStream() throws IOException {
InputStream inStream = this.getInputStreamImpl();
if (inStream == null)
throw new IOException(
"Ne peut pas obtenir le flux d'entrée de la partie " + uri);
return inStream;
}
...
protected abstract InputStream getInputStreamImpl();
...
/// Classe ZipPackagePart
@Override
protected InputStream getInputStreamImpl() {
try {
// On utilise la méthode getInputStream() de la classe java.util.zip.ZipFile qui
renvoie un InputStream vers le contenu de l’entrée donnée en paramètre.
Le paramètre zipEntry est l’entrée du zip de la partie dont nous voulons le contenu.
return container.getArchive().getInputStream(zipEntry);
} catch (IOException e) {
return null;
}
}
Listing 6 (classe PackagePart et ZipPackagePart)
Dans un second temps, nous utilisons ce flux de lecture pour lire le contenu de la partie (ici le contenu du document) :
...
Package p = Package.open(..., PackageAccess.Read);
// Obtention de la relation de la partie du contenu du document
PackageRelationship coreDocRelationship = p.getRelationshipsByType(
PackageRelationshipConstants.TYPE_CORE_DOCUMENT)
.getRelationship(0);
// Obtention de la partie du contenu du document à partir de cette
// relation (fichier XML)
PackagePart corePart = p.getPart(coreDocRelationship);
try {
// On récupére le flux de lecture de la partie
BufferedReader buffReader = new BufferedReader(
new InputStreamReader(corePart.getInputStream()));
// Lecture du contenu
String buff;
while ((buff = buffReader.readLine()) != null)
System.out.println(buff);
buffReader.close();
} catch (IOException ioe) {
System.err.println("Erreur de lecteur de la partie : "
+ corePart.getUri());
}
Listing 7
Le contenu de la partie s’affiche :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:o12="http:"//schemas.microsoft.com/office/2004/7/core
...
Haut de pageLes relations
Vous vous demandez surement comment Office 2007 arrive à retrouver les différentes parties afin de recomposer le document dans son intégralité ? Tout simplement, grâce aux fameuses relations dont nous évoquions la présence depuis le début mais sans jamais nous attarder dessus. Les relations peuvent être assimilées au « ciment » qui assure la cohésion du document en stockant tous les liens entres les différentes parties du package. Il existe deux types de relations : les relations de package (lient une partie à un package) et les relations de partie (lient une partie à une autre).
Un fichier de relations se trouve par convention dans un répertoire _rels, situé directement à la racine de la source de la relation, et porte l’extension .rels. Le nom complet du fichier de relation est composé du nom du fichier auquel les relations dépendent suivi de cette extension. Par exemple, le contenu d’un document Word possédant un URI word/document.xml sera associé à un fichier de relations ayant l’URI : word/_rels/document.xml.rels.
Fichier de relation du package
Le fichier de relation du package possède l’URI _rels/.rels, sa présence est tout aussi obligatoire que celle du fichier [Content_Types].xml. Ce fichier constitue le point de départ de l’exploration du document : il nous informe de l’emplacement du contenu principal et des propriétés du document, et pour certains types de document, par exemple PowerPoint, d’une image d’aperçu.
.jpg)
Voyons comment est structuré un fichier de relations, celui du package par exemple :
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml" />
<Relationship Id="rId2"
Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml" />
<Relationship Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="word/document.xml" />
</Relationships>
Le contenu de ce fichier de relations nous apporte plusieurs informations, à savoir dans un premier temps, la composition d’une relation : un identifiant unique, un type et enfin l’URI du fichier cible ; et dans un second temps, des informations sur la structure du document, puisque nous constatons l’existence d’un fichier de propriétés core.xml et d’un autre de propriétés étendues app.xml (nous reviendrons sur ces fichiers dans le prochain article traitant des documents). La troisième relation word/document.xml est celle de la partie du contenu principal du document sur lequel nous reviendrons dans la deuxième partie de cet article.
Une relation possède aussi une autre propriété, qui n’apparaît pas dans notre exemple car sa présence est optionnelle, c’est l’attribut TargetMode qui spécifie si la cible est une ressource interne au document - comportement par défaut - ou alors externe.
Dans cet exemple, nous allons parser le fichier de relations avec la même méthode que dans le listing 6 : nous parcourrons tous les éléments Relationship du document XML avec DOM en prenant soin d’extraire toutes les valeurs, puis nous ajouterons l’objet ainsi reconstitué à la collection de relations du paquet :
private static final String ID_ATTRIBUTE_NAME = "Id";
private static final String RELATIONSHIP_TAG_NAME = "Relationship";
private static final String TARGET_ATTRIBUTE_NAME = "Target";
private static final String TARGET_MODE_ATTRIBUTE_NAME = "TargetMode";
private static final String TYPE_ATTRIBUTE_NAME = "Type";
...
Document xmlRelationshipsDoc = documentBuilder.parse(relPart
.getInputStream());
// On parcourt les types par défaut
NodeList rels = xmlRelationshipsDoc
.getElementsByTagName(RELATIONSHIP_TAG_NAME);
for (int i = 0; i < rels.getLength(); ++i="") {
// On ajoute l'élément du type par défaut dans sa collection
Node rel = rels.item(i);
// L'identificant unique de la relation
String id = rel.getAttributes().getNamedItem(ID_ATTRIBUTE_NAME)
.getNodeValue();
// Le type de la relation
String type = rel.getAttributes().getNamedItem(
TYPE_ATTRIBUTE_NAME).getNodeValue();
// L'élément Target converti en URI
URI target;
try {
target = new URI(rel.getAttributes().getNamedItem(
TARGET_ATTRIBUTE_NAME).getNodeValue());
} catch (URISyntaxException e) {
continue;
}
// L'élément TargetMode (valeur par défaut "Internal")
Node targetModeAttr = rel.getAttributes().getNamedItem(
TARGET_MODE_ATTRIBUTE_NAME);
TargetMode targetMode = TargetMode.INTERNAL;
if (targetModeAttr != null)
targetMode = targetModeAttr.getNodeValue().toLowerCase()
.equals("internal") ? TargetMode.INTERNAL
: TargetMode.EXTERNAL;
addRelationship(target, targetMode, type, id);
}
...
Listing 8 (classe PackageRelationshipCollection)
En utilisant la classe Package, la tâche d’extraction des relations du package est rendue relativement simple :
Package p = Package.open(...);
// Récupérer toutes les relations du package
for (PackageRelationship rel : p.getRelationships()) {
System.out.println(rel.getTargetUri() + " -> "
+ rel.getRelationshipType());
}
Listing 9
Le résultat :
word/document.xml -> http://schemas.openxmlformats.org/.../officeDocument
docProps/core.xml -> http://schemas.openxmlformats.org/.../metadata/core-properties
docProps/app.xml -> http://schemas.openxmlformats.org/.../extended-properties
Fichier de relations d’une partie
Maintenant que nous avons récupéré les relations au niveau du paquet, notamment la relation vers la partie principale du contenu du document (dans notre exemple : word/document.xml), nous allons pouvoir extraire toutes les relations des ressources associées à cette partie en consultant son fichier de relations :
// Ouverture du document
Package p = Package.open(...);
// Recherche de la relation de la partie du contenu du document
PackageRelationship coreDocRelationship = null;
for (PackageRelationship rel : p.getRelationships()){
if (rel.getRelationshipType().equals(PackageRelationshipConstants.TYPE_CORE_DOCUMENT)){
coreDocRelationship = rel;
break;
}
}
// Obtention de la partie du contenu du document à partir de cette relation
PackagePart coreDocument = p.getPart(coreDocRelationship);
// Parcours des relations de la partie principale
for (PackageRelationship rel : coreDocument.getRelationships())
System.out.println(rel.getTargetUri() + " -> "
+ rel.getRelationshipType());
Listing 10
Le résultat du listing 10 : les ressources associées au contenu principal du document :
styles.xml -> http://schemas.openxmlformats.org/.../styles
settings.xml -> http://schemas.openxmlformats.org/.../settings
webSettings.xml -> http://schemas.openxmlformats.org/.../webSettings
footnotes.xml -> http://schemas.openxmlformats.org/.../footnotes
endnotes.xml -> http://schemas.openxmlformats.org/.../endnotes
media/image1.png -> http://schemas.openxmlformats.org/.../image
fontTable.xml -> http://schemas.openxmlformats.org/.../fontTable
theme/theme1.xml -> http://schemas.openxmlformats.org/.../theme
C’est selon ce principe que nous pouvons récupérer les ressources d’un package, il suffit de parcourir les parties de relation en relation jusqu’à la ressource désirée :
.jpg)
Haut de pageLes objets embarqués
La grande flexibilité du format OpenXML vous donne la possibilité d’embarquer n’importe quel objet dans le document, aussi bien textuel que binaire, sans avoir recours au pénible et limité encodage Base64.
Par exemple, si vous décidez d’ajouter une image de type PNG ou JPG dans un document, voici les étapes que réalisera un générateur de documents OpenXML tel que Office 2007 :
- Un type de contenu (image/png ou image/jpg) est ajouté au fichier de types de contenu [Content_Types].xml en le liant à l’extension .png ou .jpg selon le type d’image,
- Un répertoire media est créé, s’il n’existe pas déjà, et le fichier PNG ou JPG est stocké à cet endroit,
- La structure XML du fichier word/document.xml est modifiée afin d’ajouter une référence vers une nouvelle relation,
- Cette nouvelle relation est créée dans le fichier de relations document.xml.rels, en ajoutant un élément Relationship avec l’ID de référence, le type de la relation et la partie à laquelle la relation fait référence, ici l’image.
Le code de l’exemple suivant extrait toutes les images de type PNG dans un répertoire ‘export’ créé préalablement :
final String contentTypePNGImage = "image/png";
final String APP_ROOT = System.getProperty("user.dir") + File.separator;
// Créé le répertoire 'export' à la racine du projet avant l'exécution
final String EXPORT_FOLDER = APP_ROOT + "export" + File.separator;
ZipFile zipFile = null;
try {
zipFile = new ZipFile(APP_ROOT + "sample.docx");
} catch (IOException e) {
...
}
OpenXMLDocument doc = new OpenXMLDocument(Package.open(zipFile,
PackageAccess.Read));
doc.extractFiles(contentTypePNGImage, new File(EXPORT_FOLDER));
Listing 11
Haut de pageConclusion
La première partie de cet article vous a présenté la structure d’un package OpenXML commune à tous les documents OpenXML.
Dans la seconde partie de cet article, nous nous intéresserons à la manipulation des documents : création d’un document, modification des propriétés, transformation XSLT vers XHTML ...
Haut de pageRéférences
Haut de page