Enable specifying the value for dcterms:modified value (#27)

* pom: set version to 1.2.0

* pom: specify dependency versions as properties

* pom: add assertj-core as test dependency

* Add test for setting modified metadata value

* Revert "pom: set version to 1.2.0"

No change was needed waranting a ‘minor’ version change.

This reverts commit 5051fcf6bac670fffbf5fdfbb769818fee8cf637.

* Override default dcterms:modified value if provided

* pom: version set to 1.2.0
This commit is contained in:
Paul Campbell 2022-03-26 18:29:39 +00:00 committed by GitHub
parent 5cd01918c4
commit 6b450a36df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 351 additions and 290 deletions

26
pom.xml
View file

@ -9,9 +9,8 @@
<relativePath/> <relativePath/>
</parent> </parent>
<groupId>net.kemitix</groupId>
<artifactId>epub-creator</artifactId> <artifactId>epub-creator</artifactId>
<version>1.1.0</version> <version>1.2.0</version>
<scm> <scm>
<connection>scm:git:git@github.com:kemitix/epub-creator.git</connection> <connection>scm:git:git@github.com:kemitix/epub-creator.git</connection>
@ -27,38 +26,49 @@
<tiles-maven-plugin.version>2.22</tiles-maven-plugin.version> <tiles-maven-plugin.version>2.22</tiles-maven-plugin.version>
<kemitix-tiles.version>2.8.0</kemitix-tiles.version> <kemitix-tiles.version>2.8.0</kemitix-tiles.version>
<assertj.version>3.22.0</assertj.version>
<lombok.version>1.18.20</lombok.version>
<commons-collections.version>3.2.2</commons-collections.version>
<commons-io.version>2.10.0</commons-io.version>
<htmlcleaner.version>2.24</htmlcleaner.version>
<junit.version>4.13.2</junit.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
<version>4.13.2</version> <version>${junit.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.sourceforge.htmlcleaner</groupId> <groupId>net.sourceforge.htmlcleaner</groupId>
<artifactId>htmlcleaner</artifactId> <artifactId>htmlcleaner</artifactId>
<version>2.24</version> <version>${htmlcleaner.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>2.10.0</version> <version>${commons-io.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-collections</groupId> <groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId> <artifactId>commons-collections</artifactId>
<version>3.2.2</version> <version>${commons-collections.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.20</version> <version>${lombok.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -1,234 +1,246 @@
/* Copyright 2014 OpenCollab. /* Copyright 2014 OpenCollab.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights * in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is * copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions: * furnished to do so, subject to the following conditions:
* *
* The above copyright notice and this permission notice shall be included in * The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software. * all copies or substantial portions of the Software.
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package coza.opencollab.epub.creator.impl; package coza.opencollab.epub.creator.impl;
import coza.opencollab.epub.creator.EpubConstants; import coza.opencollab.epub.creator.EpubConstants;
import coza.opencollab.epub.creator.api.MetadataItem; import coza.opencollab.epub.creator.api.MetadataItem;
import coza.opencollab.epub.creator.api.OpfCreator; import coza.opencollab.epub.creator.api.OpfCreator;
import coza.opencollab.epub.creator.model.Content; import coza.opencollab.epub.creator.model.Content;
import coza.opencollab.epub.creator.model.EpubBook; import coza.opencollab.epub.creator.model.EpubBook;
import org.htmlcleaner.CleanerProperties; import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.ContentNode; import org.htmlcleaner.ContentNode;
import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.PrettyXmlSerializer; import org.htmlcleaner.PrettyXmlSerializer;
import org.htmlcleaner.Serializer; import org.htmlcleaner.Serializer;
import org.htmlcleaner.TagNode; import org.htmlcleaner.TagNode;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional;
/**
* Default implementation of the OpfCreator. This follows EPUB3 standards to /**
* create the OPF file content. * Default implementation of the OpfCreator. This follows EPUB3 standards to
* * create the OPF file content.
* @author OpenCollab *
*/ * @author OpenCollab
public class OpfCreatorDefault implements OpfCreator { */
public class OpfCreatorDefault implements OpfCreator {
/**
* The template XML used to create the OPF file. This is settable if a /**
* different template needs to be used. * The template XML used to create the OPF file. This is settable if a
*/ * different template needs to be used.
private String opfXML = EpubConstants.OPF_XML; */
private String opfXML = EpubConstants.OPF_XML;
/**
* HtmlCleaner used to clean the XHTML document /**
*/ * HtmlCleaner used to clean the XHTML document
private final HtmlCleaner cleaner; */
private final HtmlCleaner cleaner;
/**
* XmlSerializer used to format to XML String output /**
*/ * XmlSerializer used to format to XML String output
private final Serializer htmlSetdown; */
private final Serializer htmlSetdown;
private final List<MetadataItem> metadataItems = new ArrayList<>();
private final List<MetadataItem> metadataItems = new ArrayList<>();
public OpfCreatorDefault() {
cleaner = new HtmlCleaner(); public OpfCreatorDefault() {
CleanerProperties htmlProperties = cleaner.getProperties(); cleaner = new HtmlCleaner();
htmlProperties.setOmitHtmlEnvelope(true); CleanerProperties htmlProperties = cleaner.getProperties();
htmlProperties.setAdvancedXmlEscape(false); htmlProperties.setOmitHtmlEnvelope(true);
htmlProperties.setUseEmptyElementTags(true); htmlProperties.setAdvancedXmlEscape(false);
htmlSetdown = new PrettyXmlSerializer(htmlProperties); htmlProperties.setUseEmptyElementTags(true);
} htmlSetdown = new PrettyXmlSerializer(htmlProperties);
}
@Override
public void addMetadata(MetadataItem metadataItem) { @Override
this.metadataItems.add(metadataItem); public void addMetadata(MetadataItem metadataItem) {
} this.metadataItems.add(metadataItem);
}
/**
* {@inheritDoc} /**
*/ * {@inheritDoc}
@Override */
public String createOpfString(EpubBook book) { @Override
TagNode tagNode = cleaner.clean(opfXML); public String createOpfString(EpubBook book) {
addMetaDataTags(tagNode, book); TagNode tagNode = cleaner.clean(opfXML);
addManifestTags(tagNode, book); addMetaDataTags(tagNode, book);
addSpineTags(tagNode, book); addManifestTags(tagNode, book);
addCustomMetadata(tagNode, book); addSpineTags(tagNode, book);
return htmlSetdown.getAsString(tagNode); addCustomMetadata(tagNode, book);
} return htmlSetdown.getAsString(tagNode);
}
private void addCustomMetadata(TagNode tagNode, EpubBook book) {
TagNode metaNode = tagNode.findElementByName("metadata", true); private void addCustomMetadata(TagNode tagNode, EpubBook book) {
metadataItems.forEach(item -> { TagNode metaNode = tagNode.findElementByName("metadata", true);
TagNode node = new TagNode(item.getName()); metadataItems.forEach(item -> {
if (item.hasId()) { TagNode node = new TagNode(item.getName());
node.addAttribute("id", item.getId()); if (item.hasId()) {
} node.addAttribute("id", item.getId());
if (item.hasProperty()) { }
node.addAttribute("property", item.getProperty()); if (item.hasProperty()) {
} node.addAttribute("property", item.getProperty());
if (item.hasRefines()) { }
node.addAttribute("refines", item.getRefines()); if (item.hasRefines()) {
} node.addAttribute("refines", item.getRefines());
if (item.hasValue()) { }
node.addChild(new ContentNode(item.getValue())); if (item.hasValue()) {
} node.addChild(new ContentNode(item.getValue()));
metaNode.addChild(node); }
}); metaNode.addChild(node);
} });
}
/**
* Add the required meta data /**
* * Add the required meta data
* @param tagNode the HTML tagNode of the OPF template *
* @param book the EpubBook * @param tagNode the HTML tagNode of the OPF template
*/ * @param book the EpubBook
private void addMetaDataTags(TagNode tagNode, EpubBook book) { */
TagNode metaNode = tagNode.findElementByName("metadata", true); private void addMetaDataTags(TagNode tagNode, EpubBook book) {
addNodeData(metaNode, "dc:identifier", book.getId()); TagNode metaNode = tagNode.findElementByName("metadata", true);
addNodeData(metaNode, "dc:title", book.getTitle()); addNodeData(metaNode, "dc:identifier", book.getId());
addNodeData(metaNode, "dc:language", book.getLanguage()); addNodeData(metaNode, "dc:title", book.getTitle());
addNodeData(metaNode, "meta", new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(new Date())); addNodeData(metaNode, "dc:language", book.getLanguage());
if (book.getAuthor() != null) { Optional<MetadataItem> customModifiedValue = metadataItems.stream()
TagNode creatorNode = new TagNode("dc:creator"); .filter(MetadataItem::hasValue)
creatorNode.addChild(new ContentNode(book.getAuthor())); .filter(MetadataItem::hasProperty)
metaNode.addChild(creatorNode); .filter(item -> item.getProperty().equals("dcterms:modified"))
} .findFirst();
} if (customModifiedValue.isPresent()) {
MetadataItem item = customModifiedValue.get();
/** addNodeData(metaNode, "meta", item.getValue());
* Adds a item tag to the manifest for each Content object. metadataItems.remove(item);
* } else {
* The manifest contains all Content that will be added to the EPUB as files addNodeData(metaNode, "meta", new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(new Date()));
* }
* @param tagNode the HTML tagNode of the OPF template if (book.getAuthor() != null) {
* @param book the EpubBook TagNode creatorNode = new TagNode("dc:creator");
*/ creatorNode.addChild(new ContentNode(book.getAuthor()));
private void addManifestTags(TagNode tagNode, EpubBook book) { metaNode.addChild(creatorNode);
TagNode manifestNode = tagNode.findElementByName("manifest", true); }
for (Content content : book.getContents()) { }
manifestNode.addChild(buildItemNode(content));
} /**
} * Adds a item tag to the manifest for each Content object.
*
/** * The manifest contains all Content that will be added to the EPUB as files
* Builds an item tag from the Content object *
* * @param tagNode the HTML tagNode of the OPF template
* @param content * @param book the EpubBook
* @return */
*/ private void addManifestTags(TagNode tagNode, EpubBook book) {
private TagNode buildItemNode(Content content) { TagNode manifestNode = tagNode.findElementByName("manifest", true);
TagNode itemNode = new TagNode("item"); for (Content content : book.getContents()) {
itemNode.addAttribute("href", content.getHref()); manifestNode.addChild(buildItemNode(content));
itemNode.addAttribute("id", content.getId()); }
itemNode.addAttribute("media-type", content.getMediaType()); }
if (content.getProperties() != null) {
itemNode.addAttribute("properties", content.getProperties()); /**
} * Builds an item tag from the Content object
if (content.hasFallBack()) { *
itemNode.addAttribute("fallback", content.getFallBack().getId()); * @param content
} * @return
return itemNode; */
} private TagNode buildItemNode(Content content) {
TagNode itemNode = new TagNode("item");
/** itemNode.addAttribute("href", content.getHref());
* Adds item ref tags for all Content objects that must be added to the itemNode.addAttribute("id", content.getId());
* spine. itemNode.addAttribute("media-type", content.getMediaType());
* if (content.getProperties() != null) {
* The spine contains all the resources that will be shown when reading the itemNode.addAttribute("properties", content.getProperties());
* book from start to end }
* if (content.hasFallBack()) {
* @param tagNode the HTML tagNode of the OPF template itemNode.addAttribute("fallback", content.getFallBack().getId());
* @param book the EpubBook }
*/ return itemNode;
private void addSpineTags(TagNode tagNode, EpubBook book) { }
TagNode spineNode = tagNode.findElementByName("spine", true);
for (Content content : book.getContents()) { /**
if (content.isSpine()) { * Adds item ref tags for all Content objects that must be added to the
spineNode.addChild(buildItemrefNode(content)); * spine.
} *
} * The spine contains all the resources that will be shown when reading the
} * book from start to end
*
/** * @param tagNode the HTML tagNode of the OPF template
* Builds an item ref tag from the Content object * @param book the EpubBook
* */
* @param content private void addSpineTags(TagNode tagNode, EpubBook book) {
* @return TagNode spineNode = tagNode.findElementByName("spine", true);
*/ for (Content content : book.getContents()) {
private TagNode buildItemrefNode(Content content) { if (content.isSpine()) {
TagNode itemNode = new TagNode("itemref"); spineNode.addChild(buildItemrefNode(content));
itemNode.addAttribute("idref", content.getId()); }
if (!content.isLinear()) { }
itemNode.addAttribute("linear", "no"); }
}
return itemNode; /**
} * Builds an item ref tag from the Content object
*
/** * @param content
* Adds a ContentNode (value) with to a child element of the TagNode * @return
* */
* <elementName>{value}<elementName> private TagNode buildItemrefNode(Content content) {
* TagNode itemNode = new TagNode("itemref");
* @param tagNode itemNode.addAttribute("idref", content.getId());
* @param elementName if (!content.isLinear()) {
* @param value itemNode.addAttribute("linear", "no");
*/ }
private void addNodeData(TagNode tagNode, String elementName, String value) { return itemNode;
TagNode editNode = tagNode.findElementByName(elementName, true); }
editNode.addChild(new ContentNode(value));
} /**
* Adds a ContentNode (value) with to a child element of the TagNode
/** *
* The base XML used for the OPF file. * <elementName>{value}<elementName>
* *
* @return the OPF XML text * @param tagNode
*/ * @param elementName
public String getOpfXML() { * @param value
return opfXML; */
} private void addNodeData(TagNode tagNode, String elementName, String value) {
TagNode editNode = tagNode.findElementByName(elementName, true);
/** editNode.addChild(new ContentNode(value));
* The base XML used for the OPF file. This is optional as there is a EPUB3 }
* standard default but it can be overridden.
* /**
* @param opfXML the OPF XML to set * The base XML used for the OPF file.
*/ *
public void setOpfXML(String opfXML) { * @return the OPF XML text
this.opfXML = opfXML; */
} public String getOpfXML() {
return opfXML;
} }
/**
* The base XML used for the OPF file. This is optional as there is a EPUB3
* standard default but it can be overridden.
*
* @param opfXML the OPF XML to set
*/
public void setOpfXML(String opfXML) {
this.opfXML = opfXML;
}
}

View file

@ -1,48 +1,87 @@
package coza.opencollab.epub.creator; package coza.opencollab.epub.creator;
import coza.opencollab.epub.creator.api.MetadataItem; import coza.opencollab.epub.creator.api.MetadataItem;
import coza.opencollab.epub.creator.model.EpubBook; import coza.opencollab.epub.creator.model.EpubBook;
import java.io.File; import lombok.SneakyThrows;
import java.io.FileOutputStream; import lombok.val;
import junit.framework.Assert; import org.apache.commons.io.IOUtils;
import org.apache.commons.io.IOUtils; import org.assertj.core.api.WithAssertions;
import org.junit.Test; import org.junit.Test;
/** import java.io.File;
* import java.io.FileOutputStream;
* @author OpenCollab import java.io.OutputStream;
*/ import java.util.Scanner;
public class EpubCreatorTest { import java.util.zip.ZipFile;
@Test /**
public void testEpubCreate() { * @author OpenCollab
try (FileOutputStream file = new FileOutputStream(new File("test.epub"))) { */
EpubBook book = new EpubBook("en", "Samuel .-__Id1", "Samuel Test Book", "Samuel Holtzkampf"); public class EpubCreatorTest implements WithAssertions {
MetadataItem.Builder builder = MetadataItem.builder(); String author = "Samuel Holtzkampf";
book.addMetadata(builder.name("dc:creator").value("Bob Smith")); String modified = "modified-date-and-time";
book.addMetadata(builder.name("meta")
.property("role").refines("#editor-id") @Test
.value("Editor")); public void bookHasAuthor() {
//when
book.addContent(this.getClass().getResourceAsStream("/epub30-overview.xhtml"), val book = createEpubBook();
"application/xhtml+xml", "xhtml/epub30-overview.xhtml", true, true).setId("Overview"); //then
book.addContent(this.getClass().getResourceAsStream("/idpflogo_web_125.jpg"), assertThat(book.getAuthor()).isEqualTo(author);
"image/jpeg", "img/idpflogo_web_125.jpg", false, false);
book.addContent(this.getClass().getResourceAsStream("/epub-spec.css"), }
"text/css", "css/epub-spec.css", false, false);
book.addTextContent("TestHtml", "xhtml/samuelTest2.xhtml", "Samuel test one two four!!!!!\nTesting two").setToc(true); @Test
book.addTextContent("TestHtml", "xhtml/samuelTest.xhtml", "Samuel test one two three\nTesting two").setToc(true); public void hasSetModifiedValue() {
book.addCoverImage(IOUtils.toByteArray(this.getClass().getResourceAsStream("/P1010832.jpg")), //given
"image/jpeg", "images/P1010832.jpg"); //TODO use a proper temp file
val file = new File("test.epub");
writeBookToFile(createEpubBook(), file);
book.writeToStream(file); //when
// TODO : real tests to see if document correct, this is just to test that creation is succesfull String bookOpf = unzipFileEntry(file, "content/book.opf");
Assert.assertEquals("test", "test"); //then
} catch (Exception ex) { assertThat(bookOpf).containsOnlyOnce("<meta property=\"dcterms:modified\">");
System.out.println(ex); assertThat(bookOpf).contains(String.format("<meta property=\"dcterms:modified\">%s</meta>", modified));
Assert.assertEquals("test", "test1"); }
}
} @SneakyThrows
} private String unzipFileEntry(File file, String name) {
val zipFile = new ZipFile(file);
val entry = zipFile.getEntry(name);
val inputStream = zipFile.getInputStream(entry);
try (Scanner scanner = new Scanner(inputStream)) {
return scanner.useDelimiter("\\A").next();
}
}
@SneakyThrows
private void writeBookToFile(EpubBook book, File file) {
try (OutputStream outputStream = new FileOutputStream(file)) {
book.writeToStream(outputStream);
}
}
@SneakyThrows
private EpubBook createEpubBook() {
EpubBook book = new EpubBook("en", "Samuel .-__Id1", "Samuel Test Book", author);
MetadataItem.Builder builder = MetadataItem.builder();
book.addMetadata(builder.name("dc:creator").value("Bob Smith"));
book.addMetadata(builder.name("meta")
.property("role").refines("#editor-id")
.value("Editor"));
book.addMetadata((builder.name("meta").property("dcterms:modified").value(modified)));
book.addContent(this.getClass().getResourceAsStream("/epub30-overview.xhtml"),
"application/xhtml+xml", "xhtml/epub30-overview.xhtml", true, true).setId("Overview");
book.addContent(this.getClass().getResourceAsStream("/idpflogo_web_125.jpg"),
"image/jpeg", "img/idpflogo_web_125.jpg", false, false);
book.addContent(this.getClass().getResourceAsStream("/epub-spec.css"),
"text/css", "css/epub-spec.css", false, false);
book.addTextContent("TestHtml", "xhtml/samuelTest2.xhtml", "Samuel test one two four!!!!!\nTesting two").setToc(true);
book.addTextContent("TestHtml", "xhtml/samuelTest.xhtml", "Samuel test one two three\nTesting two").setToc(true);
book.addCoverImage(IOUtils.toByteArray(this.getClass().getResourceAsStream("/P1010832.jpg")),
"image/jpeg", "images/P1010832.jpg");
return book;
}
}