| /******************************************************************************* |
| * 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); |
| } |
| } |
| |
| } |