blob: 53f65345674cad82273f2dbd0baff62acb063944 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2014 Torkild U. Resheim.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Torkild U. Resheim - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.docs.epub.core;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.emf.ecore.xmi.XMLHelper;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.mylyn.docs.epub.core.ILogger.Severity;
import org.eclipse.mylyn.docs.epub.ncx.DocTitle;
import org.eclipse.mylyn.docs.epub.ncx.Head;
import org.eclipse.mylyn.docs.epub.ncx.Meta;
import org.eclipse.mylyn.docs.epub.ncx.NCXFactory;
import org.eclipse.mylyn.docs.epub.ncx.NCXPackage;
import org.eclipse.mylyn.docs.epub.ncx.NavMap;
import org.eclipse.mylyn.docs.epub.ncx.Ncx;
import org.eclipse.mylyn.docs.epub.ncx.Text;
import org.eclipse.mylyn.docs.epub.ncx.util.NCXResourceFactoryImpl;
import org.eclipse.mylyn.docs.epub.ncx.util.NCXResourceImpl;
import org.eclipse.mylyn.docs.epub.opf.Guide;
import org.eclipse.mylyn.docs.epub.opf.Item;
import org.eclipse.mylyn.docs.epub.opf.Itemref;
import org.eclipse.mylyn.docs.epub.opf.Manifest;
import org.eclipse.mylyn.docs.epub.opf.Metadata;
import org.eclipse.mylyn.docs.epub.opf.OPFFactory;
import org.eclipse.mylyn.docs.epub.opf.Spine;
import org.eclipse.mylyn.internal.docs.epub.core.EPUBXMLHelperImp;
import org.eclipse.mylyn.internal.docs.epub.core.OPSValidator;
import org.eclipse.mylyn.internal.docs.epub.core.TOCGenerator;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* This type represents one EPUB revision 2.0.1 formatted publication.
*
* @author Torkild U. Resheim
*/
public class OPSPublication extends Publication {
/** MIME type for NCX documents */
private static final String MIMETYPE_NCX = "application/x-dtbncx+xml"; //$NON-NLS-1$
private static final String NCX_FILE_SUFFIX = "ncx"; //$NON-NLS-1$
/** Identifier of the table of contents file */
private static final String TABLE_OF_CONTENTS_ID = "ncx"; //$NON-NLS-1$
/** Default name for the table of contents */
private static final String TOCFILE_NAME = "toc.ncx"; //$NON-NLS-1$
/** List of core media types as specified in http://idpf.org/epub/20/spec/OPS_2.0.1_draft.htm#Section1.3.7 */
private static final String[] CORE_MEDIA_TYPES = new String[] { "image/gif", "image/jpeg", "image/png", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"image/svg+xml", "application/xhtml+xml", "application/x-dtbook+xml", "text/css", "application/xml", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
"text/x-oeb1-document", "text/x-oeb1-css", "application/x-dtbncx+xml" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
/** The table of contents */
private Ncx ncxTOC;
/**
* Creates a new EPUB.
*/
public OPSPublication() {
super();
setup();
}
/**
* Creates a new EPUB logging all event to the specified logger.
*/
public OPSPublication(ILogger logger) {
super(logger);
setup();
}
/**
* This mechanism will traverse the spine of the publication (which is representing the reading order) and parse
* each file for information that can be used to assemble a table of contents. Only XHTML type of files will be
* taken into consideration.
*
* @throws SAXException
* @throws IOException
* @throws ParserConfigurationException
*/
@Override
protected void generateTableOfContents() throws ParserConfigurationException, SAXException, IOException {
log(Messages.getString("OPS2Publication.0"), Severity.INFO, indent++); //$NON-NLS-1$
Meta meta = NCXFactory.eINSTANCE.createMeta();
meta.setName("dtb:uid"); //$NON-NLS-1$
meta.setContent(getIdentifier().getMixed().getValue(0).toString());
ncxTOC.getHead().getMetas().add(meta);
int playOrder = 0;
// Iterate over the spine
EList<Itemref> spineItems = getSpine().getSpineItems();
EList<Item> manifestItems = opfPackage.getManifest().getItems();
for (Itemref itemref : spineItems) {
Item referencedItem = null;
String id = itemref.getIdref();
// Find the manifest item that is referenced
for (Item item : manifestItems) {
if (item.getId().equals(id)) {
referencedItem = item;
break;
}
}
if (referencedItem != null && !referencedItem.isNoToc()
&& referencedItem.getMedia_type().equals(MIMETYPE_XHTML)) {
File file = new File(referencedItem.getFile());
FileInputStream fis = new FileInputStream(file);
log(MessageFormat.format(Messages.getString("OPS2Publication.1"), referencedItem.getHref()), Severity.VERBOSE, indent); //$NON-NLS-1$
playOrder = TOCGenerator.parse(new InputSource(fis), referencedItem.getHref(), ncxTOC, playOrder);
}
}
indent--;
}
@Override
public Object getTableOfContents() {
return ncxTOC;
}
@Override
protected String getVersion() {
return "2.0"; //$NON-NLS-1$
}
@Override
protected void readTableOfContents(File tocFile) throws IOException {
ResourceSet resourceSet = new ResourceSetImpl();
URI fileURI = URI.createFileURI(tocFile.getAbsolutePath());
Resource resource = resourceSet.createResource(fileURI);
resource.load(null);
ncxTOC = (Ncx) resource.getContents().get(0);
}
/**
* Registers a new resource factory for NCX data structures. This is normally done through Eclipse extension points
* but we also need to be able to create this factory without the Eclipse runtime.
*/
private void registerNCXResourceFactory() {
// Register package so that it is available even without the Eclipse runtime
@SuppressWarnings("unused")
NCXPackage packageInstance = NCXPackage.eINSTANCE;
Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put(NCX_FILE_SUFFIX,
new NCXResourceFactoryImpl() {
@Override
public Resource createResource(URI uri) {
NCXResourceImpl xmiResource = new NCXResourceImpl(uri) {
@Override
protected XMLHelper createXMLHelper() {
EPUBXMLHelperImp xmlHelper = new EPUBXMLHelperImp();
return xmlHelper;
}
};
Map<Object, Object> loadOptions = xmiResource.getDefaultLoadOptions();
Map<Object, Object> saveOptions = xmiResource.getDefaultSaveOptions();
// We use extended metadata
saveOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, Boolean.TRUE);
loadOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, Boolean.TRUE);
// Required in order to correctly read in attributes
loadOptions.put(XMLResource.OPTION_LAX_FEATURE_PROCESSING, Boolean.TRUE);
// Treat "href" attributes as features
loadOptions.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE);
// UTF-8 encoding is required per specification
saveOptions.put(XMLResource.OPTION_ENCODING, XML_ENCODING);
// Do not download any external DTDs.
Map<String, Object> parserFeatures = new HashMap<String, Object>();
parserFeatures.put("http://xml.org/sax/features/validation", Boolean.FALSE); //$NON-NLS-1$
parserFeatures.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", //$NON-NLS-1$
Boolean.FALSE);
loadOptions.put(XMLResource.OPTION_PARSER_FEATURES, parserFeatures);
return xmiResource;
}
});
}
@Override
public void setTableOfContents(File ncxFile) {
// Add the file to the publication and make sure we use the table of
// contents identifier.
Item item = addItem(opfPackage.getSpine().getToc(), null, ncxFile, null, MIMETYPE_NCX, false, false, false);
// The table of contents file must be first.
opfPackage.getManifest().getItems().move(0, item);
log(MessageFormat.format("Using table of contents file {0} for OPS", new Object[] { ncxFile.getName() }), //$NON-NLS-1$
Severity.VERBOSE, indent);
}
private void setup() {
opfPackage.setVersion(getVersion());
ncxTOC = NCXFactory.eINSTANCE.createNcx();
// Set the required version attribute
ncxTOC.setVersion("2005-1"); //$NON-NLS-1$
// Create the required head element
Head head = NCXFactory.eINSTANCE.createHead();
ncxTOC.setHead(head);
// Create the required title element
DocTitle docTitle = NCXFactory.eINSTANCE.createDocTitle();
Text text = NCXFactory.eINSTANCE.createText();
FeatureMapUtil.addText(text.getMixed(), "Table of contents"); //$NON-NLS-1$
docTitle.setText(text);
ncxTOC.setDocTitle(docTitle);
// Create the required navigation map element
NavMap navMap = NCXFactory.eINSTANCE.createNavMap();
ncxTOC.setNavMap(navMap);
// Create the required metadata element
Metadata opfMetadata = OPFFactory.eINSTANCE.createMetadata();
opfPackage.setMetadata(opfMetadata);
Guide opfGuide = OPFFactory.eINSTANCE.createGuide();
opfPackage.setGuide(opfGuide);
Manifest opfManifest = OPFFactory.eINSTANCE.createManifest();
opfPackage.setManifest(opfManifest);
// Create the spine and set a reference to the table of contents
// item which will be added to the manifest on a later stage.
Spine opfSpine = OPFFactory.eINSTANCE.createSpine();
opfSpine.setToc(TABLE_OF_CONTENTS_ID);
opfPackage.setSpine(opfSpine);
registerNCXResourceFactory();
opfPackage.setGenerateTableOfContents(true);
}
/**
* Validates all XHTML items in the manifest. The following rules are observed:
* <ul>
* <li>The item must be a core media type. If not it must have a fallback item which must exist and be of a core
* media type. Otherwise an error is added to the list of messages</li>
* <li>XHTML file content must be in the preferred vocabulary. Warnings are added when this is not the case.</li>
* </ul>
*
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
*/
@Override
protected List<ValidationMessage> validateContents() throws ParserConfigurationException, SAXException, IOException {
EList<Item> manifestItems = opfPackage.getManifest().getItems();
ArrayList<ValidationMessage> messages = new ArrayList<ValidationMessage>();
for (Item item : manifestItems) {
if (!isLegalType(item)) {
Item fallback = getItemById(item.getFallback());
if (fallback == null) {
messages.add(new ValidationMessage(ValidationMessage.Severity.WARNING, MessageFormat.format(
Messages.getString("OPS2Publication.13"), //$NON-NLS-1$
item.getHref())));
} else if (!isLegalType(fallback)) {
messages.add(new ValidationMessage(ValidationMessage.Severity.WARNING, MessageFormat.format(
Messages.getString("OPS2Publication.14"), //$NON-NLS-1$
item.getHref())));
} else {
messages.add(new ValidationMessage(ValidationMessage.Severity.WARNING, MessageFormat.format(
Messages.getString("OPS2Publication.15"), //$NON-NLS-1$
item.getHref())));
}
}
// Validate the XHTML items to see if they contain illegal attributes and elements
if (item.getMedia_type().equals(MIMETYPE_XHTML)) {
File file = new File(item.getFile());
FileReader fr = new FileReader(file);
messages.addAll(OPSValidator.validate(new InputSource(fr), item.getHref()));
}
}
return messages;
}
private boolean isLegalType(Item item) {
boolean legal = false;
for (String type : CORE_MEDIA_TYPES) {
if (item.getMedia_type().equals(type)) {
legal = true;
}
}
return legal;
}
/**
* Writes the table of contents file in the specified folder using the NCX format. If a table of contents file has
* not been specified an empty one will be created (since it is required to have one). If in addition it has been
* specified that the table of contents should be created, the content files will be parsed and a TOC will be
* generated.
*
* @param oepbsFolder
* the folder to create the NCX file in
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
* @see {@link #setTableOfContents(File)}
*/
@Override
protected void writeTableOfContents(File oepbsFolder) throws IOException, ParserConfigurationException,
SAXException {
// If a table of contents file has not been specified we must create
// one. If it has been specified it will be copied.
if (getItemById(opfPackage.getSpine().getToc()) == null) {
File ncxFile = new File(oepbsFolder.getAbsolutePath() + File.separator + TOCFILE_NAME);
ResourceSet resourceSet = new ResourceSetImpl();
// Register the packages to make it available during loading.
resourceSet.getPackageRegistry().put(NCXPackage.eNS_URI, NCXPackage.eINSTANCE);
URI fileURI = URI.createFileURI(ncxFile.getAbsolutePath());
Resource resource = resourceSet.createResource(fileURI);
// We've been asked to generate a table of contents using pages
// contained in the spine.
if (opfPackage.isGenerateTableOfContents()) {
generateTableOfContents();
}
resource.getContents().add(ncxTOC);
Map<String, Object> options = new HashMap<String, Object>();
// NCX requires that we encode using UTF-8
options.put(XMLResource.OPTION_ENCODING, XML_ENCODING);
options.put(XMLResource.OPTION_EXTENDED_META_DATA, Boolean.TRUE);
resource.save(options);
// Make sure the table of contents file is in the manifest and
// referenced in the spine. We also want it to be the first element
// in the manifest.
Item item = addItem(opfPackage.getSpine().getToc(), null, ncxFile, null, MIMETYPE_NCX, false, false, false);
opfPackage.getManifest().getItems().move(0, item);
}
}
}