Added
Link Here
|
1 |
/******************************************************************************* |
2 |
* Copyright (c) 2011 Torkild U. Resheim. |
3 |
* |
4 |
* All rights reserved. This program and the accompanying materials are made |
5 |
* available under the terms of the Eclipse Public License v1.0 which |
6 |
* accompanies this distribution, and is available at |
7 |
* http://www.eclipse.org/legal/epl-v10.html |
8 |
* |
9 |
* Contributors: |
10 |
* Torkild U. Resheim - initial API and implementation |
11 |
*******************************************************************************/ |
12 |
package org.eclipse.mylyn.docs.epub.core; |
13 |
|
14 |
import java.io.File; |
15 |
import java.io.FileWriter; |
16 |
import java.io.IOException; |
17 |
import java.text.SimpleDateFormat; |
18 |
import java.util.ArrayList; |
19 |
import java.util.HashMap; |
20 |
import java.util.List; |
21 |
import java.util.Locale; |
22 |
import java.util.Map; |
23 |
import java.util.TimeZone; |
24 |
import java.util.UUID; |
25 |
|
26 |
import javax.xml.parsers.ParserConfigurationException; |
27 |
|
28 |
import org.eclipse.emf.common.util.BasicDiagnostic; |
29 |
import org.eclipse.emf.common.util.Diagnostic; |
30 |
import org.eclipse.emf.common.util.EList; |
31 |
import org.eclipse.emf.common.util.URI; |
32 |
import org.eclipse.emf.ecore.EObject; |
33 |
import org.eclipse.emf.ecore.EValidator; |
34 |
import org.eclipse.emf.ecore.resource.Resource; |
35 |
import org.eclipse.emf.ecore.resource.ResourceSet; |
36 |
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; |
37 |
import org.eclipse.emf.ecore.util.Diagnostician; |
38 |
import org.eclipse.emf.ecore.util.EcoreValidator; |
39 |
import org.eclipse.emf.ecore.util.FeatureMapUtil; |
40 |
import org.eclipse.emf.ecore.xmi.XMLHelper; |
41 |
import org.eclipse.emf.ecore.xmi.XMLResource; |
42 |
import org.eclipse.emf.ecore.xmi.impl.XMLResourceFactoryImpl; |
43 |
import org.eclipse.mylyn.docs.epub.dc.Contributor; |
44 |
import org.eclipse.mylyn.docs.epub.dc.Coverage; |
45 |
import org.eclipse.mylyn.docs.epub.dc.Creator; |
46 |
import org.eclipse.mylyn.docs.epub.dc.DCFactory; |
47 |
import org.eclipse.mylyn.docs.epub.dc.DCType; |
48 |
import org.eclipse.mylyn.docs.epub.dc.Date; |
49 |
import org.eclipse.mylyn.docs.epub.dc.Description; |
50 |
import org.eclipse.mylyn.docs.epub.dc.Format; |
51 |
import org.eclipse.mylyn.docs.epub.dc.Identifier; |
52 |
import org.eclipse.mylyn.docs.epub.dc.Language; |
53 |
import org.eclipse.mylyn.docs.epub.dc.LocalizedDCType; |
54 |
import org.eclipse.mylyn.docs.epub.dc.Publisher; |
55 |
import org.eclipse.mylyn.docs.epub.dc.Relation; |
56 |
import org.eclipse.mylyn.docs.epub.dc.Rights; |
57 |
import org.eclipse.mylyn.docs.epub.dc.Source; |
58 |
import org.eclipse.mylyn.docs.epub.dc.Subject; |
59 |
import org.eclipse.mylyn.docs.epub.dc.Title; |
60 |
import org.eclipse.mylyn.docs.epub.opf.Item; |
61 |
import org.eclipse.mylyn.docs.epub.opf.Itemref; |
62 |
import org.eclipse.mylyn.docs.epub.opf.OPFFactory; |
63 |
import org.eclipse.mylyn.docs.epub.opf.OPFPackage; |
64 |
import org.eclipse.mylyn.docs.epub.opf.Package; |
65 |
import org.eclipse.mylyn.docs.epub.opf.Reference; |
66 |
import org.eclipse.mylyn.docs.epub.opf.Role; |
67 |
import org.eclipse.mylyn.docs.epub.opf.Spine; |
68 |
import org.eclipse.mylyn.docs.epub.opf.Type; |
69 |
import org.eclipse.mylyn.docs.epub.opf.util.OPFResourceImpl; |
70 |
import org.eclipse.mylyn.internal.docs.epub.core.EPUBFileUtil; |
71 |
import org.eclipse.mylyn.internal.docs.epub.core.EPUBXMLHelperImp; |
72 |
import org.eclipse.mylyn.internal.docs.epub.core.ReferenceScanner; |
73 |
import org.xml.sax.SAXException; |
74 |
|
75 |
/** |
76 |
* This type represents one <i>OPS publication</i>. This includes the <i>OPF |
77 |
* package document</i> and <i>OPS content documents</i>. It maintains a data |
78 |
* structure representing the entire publication and API for building it. |
79 |
* <p> |
80 |
* Please note that this API is provisional and should not yet be used to build |
81 |
* applications. |
82 |
* </p> |
83 |
* |
84 |
* @author Torkild U. Resheim |
85 |
*/ |
86 |
public abstract class OPSPublication { |
87 |
// Rules of engagement: |
88 |
// * Keep all data in the model, use "transient" for temporary variables |
89 |
// * Do not create anything before the final assemble |
90 |
|
91 |
/** Default identifier for the cover page */ |
92 |
private static final String COVER_ID = "cover"; |
93 |
|
94 |
/** Publication identifier for the cover image item */ |
95 |
public static final String COVER_IMAGE_ID = "cover-image"; |
96 |
|
97 |
protected static final String CREATION_DATE_ID = "creation"; |
98 |
|
99 |
public final static String MIMETYPE_CSS = "text/css"; |
100 |
|
101 |
public static final String MIMETYPE_EPUB = "application/epub+zip"; |
102 |
|
103 |
public static final String MIMETYPE_XHTML = "application/xhtml+xml"; |
104 |
|
105 |
private static final String OPF_FILE_SUFFIX = "opf"; |
106 |
|
107 |
protected static final String UUID_SCHEME = "uuid"; |
108 |
|
109 |
/** The encoding to use in XML files */ |
110 |
protected static final String XML_ENCODING = "UTF-8"; |
111 |
|
112 |
/** |
113 |
* Returns an EPUB version 2.0.1 instance. |
114 |
* |
115 |
* @return an EPUB instance |
116 |
*/ |
117 |
public static OPSPublication getVersion2Instance() { |
118 |
return new OPS2Publication(); |
119 |
} |
120 |
|
121 |
public List<ValidationMessage> messages; |
122 |
|
123 |
/** The root model element */ |
124 |
protected Package opfPackage; |
125 |
|
126 |
/** The root folder TODO: Move to opfPackage */ |
127 |
private File rootFolder; |
128 |
|
129 |
protected OPSPublication() { |
130 |
opfPackage = OPFFactory.eINSTANCE.createPackage(); |
131 |
registerOPFResourceFactory(); |
132 |
} |
133 |
|
134 |
/** |
135 |
* Adds data to the publication that we always want to be present. |
136 |
* <ul> |
137 |
* <li>The creation date.</li> |
138 |
* <li><i>Eclipse committers and contributors</i> as contributor redactor |
139 |
* role.</li> |
140 |
* <li>A unique identifier if none has been specified.</li> |
141 |
* <li>A empty description if none has been specified.</li> |
142 |
* <li>Language "English" if none has been specified.</li> |
143 |
* <li>A dummy title if none has been specified.</li> |
144 |
* <li>The publication format if none has been specified.</li> |
145 |
* </ul> |
146 |
*/ |
147 |
private void addCompulsoryData() { |
148 |
addDate(null, new java.util.Date(System.currentTimeMillis()), CREATION_DATE_ID); |
149 |
addContributor(null, null, "Eclipse Committers and Contributors", Role.REDACTOR, null); |
150 |
if (getIdentifier() == null) { |
151 |
addIdentifier(UUID_SCHEME, "uuid", UUID.randomUUID().toString()); |
152 |
setIdentifierId(UUID_SCHEME); |
153 |
} |
154 |
// Add empty subject |
155 |
if (opfPackage.getMetadata().getSubjects().isEmpty()) { |
156 |
addSubject(null, null, ""); |
157 |
} |
158 |
// Add English language |
159 |
if (opfPackage.getMetadata().getLanguages().isEmpty()) { |
160 |
addLanguage(null, Locale.ENGLISH.toString()); |
161 |
} |
162 |
// Add dummy title |
163 |
if (opfPackage.getMetadata().getTitles().isEmpty()) { |
164 |
addTitle(null, null, "No title specified"); |
165 |
} |
166 |
// Set the publication format |
167 |
if (opfPackage.getMetadata().getFormats().isEmpty()) { |
168 |
addFormat(null, MIMETYPE_EPUB); |
169 |
} |
170 |
} |
171 |
|
172 |
/** |
173 |
* Specifies a new contributor for the publication. |
174 |
* |
175 |
* @param id |
176 |
* an identifier or <code>null</code> |
177 |
* @param name |
178 |
* name of the creator |
179 |
* @param role |
180 |
* the role or <code>null</code> |
181 |
* @param fileAs |
182 |
* name to file the creator under or <code>null</code> |
183 |
* @param lang |
184 |
* the language code or <code>null</code> |
185 |
* @return the new creator |
186 |
*/ |
187 |
public Contributor addContributor(String id, Locale lang, String name, Role role, String fileAs) { |
188 |
Contributor dc = DCFactory.eINSTANCE.createContributor(); |
189 |
setDcLocalized(dc, id, lang, name); |
190 |
if (role != null) { |
191 |
dc.setRole(role); |
192 |
} |
193 |
if (fileAs != null) { |
194 |
dc.setFileAs(fileAs); |
195 |
} |
196 |
opfPackage.getMetadata().getContributors().add(dc); |
197 |
return dc; |
198 |
} |
199 |
|
200 |
/** |
201 |
* Specifies a new "coverage" for the publication. |
202 |
* |
203 |
* @param id |
204 |
* an identifier or <code>null</code> |
205 |
* @param lang |
206 |
* the language code or <code>null</code> |
207 |
* @param value |
208 |
* value of the item |
209 |
* @return the new coverage |
210 |
*/ |
211 |
public Coverage addCoverage(String id, Locale lang, String value) { |
212 |
if (value == null) { |
213 |
throw new IllegalArgumentException("A value must be specified"); |
214 |
} |
215 |
Coverage dc = DCFactory.eINSTANCE.createCoverage(); |
216 |
setDcLocalized(dc, id, lang, value); |
217 |
opfPackage.getMetadata().getCoverages().add(dc); |
218 |
return dc; |
219 |
} |
220 |
|
221 |
/** |
222 |
* Specifies a new creator for the publication. |
223 |
* |
224 |
* |
225 |
* @param id |
226 |
* a unique identifier or <code>null</code> |
227 |
* @param lang |
228 |
* the language code or <code>null</code> |
229 |
* @param name |
230 |
* name of the creator |
231 |
* @param role |
232 |
* the role or <code>null</code> |
233 |
* @param fileAs |
234 |
* name to file the creator under or <code>null</code> |
235 |
* @return the new creator |
236 |
*/ |
237 |
public Creator addCreator(String id, Locale lang, String name, Role role, String fileAs) { |
238 |
Creator dc = DCFactory.eINSTANCE.createCreator(); |
239 |
setDcLocalized(dc, id, lang, name); |
240 |
if (role != null) { |
241 |
dc.setRole(role); |
242 |
} |
243 |
if (fileAs != null) { |
244 |
dc.setFileAs(fileAs); |
245 |
} |
246 |
opfPackage.getMetadata().getCreators().add(dc); |
247 |
return dc; |
248 |
} |
249 |
|
250 |
/** |
251 |
* Adds a new date to the publication. The given instance will be |
252 |
* represented in a format defined by "Date and Time Formats" at |
253 |
* http://www.w3.org/TR/NOTE-datetime and by ISO 8601 on which it is based. |
254 |
* |
255 |
* @param id |
256 |
* optional identifier |
257 |
* @param date |
258 |
* the date |
259 |
* @param event |
260 |
* the event |
261 |
* @return the new date |
262 |
* @see #addDate(String, String, String) |
263 |
*/ |
264 |
public Date addDate(String id, java.util.Date date, String event) { |
265 |
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); |
266 |
TimeZone tz = TimeZone.getTimeZone("UTC"); |
267 |
df.setTimeZone(tz); |
268 |
Date dc = DCFactory.eINSTANCE.createDate(); |
269 |
setDcCommon(dc, id, df.format(date)); |
270 |
if (event != null) { |
271 |
dc.setEvent(event); |
272 |
} |
273 |
opfPackage.getMetadata().getDates().add(dc); |
274 |
return dc; |
275 |
} |
276 |
|
277 |
/** |
278 |
* Date of publication, in the format defined by "Date and Time Formats" at |
279 |
* http://www.w3.org/TR/NOTE-datetime and by ISO 8601. In particular, dates |
280 |
* without times must be represented in the form YYYY[-MM[-DD]]: a required |
281 |
* 4-digit year, an optional 2-digit month, and if the month is given, an |
282 |
* optional 2-digit day of month. The event attribute is optional, possible |
283 |
* values may include: "creation", "publication", and "modification". |
284 |
* |
285 |
* @param value |
286 |
* the date string |
287 |
* @param event |
288 |
* an optional event description |
289 |
* @return the new date |
290 |
*/ |
291 |
public Date addDate(String id, String value, String event) { |
292 |
if (value == null) { |
293 |
throw new IllegalArgumentException("A value must be specified"); |
294 |
} |
295 |
Date dc = DCFactory.eINSTANCE.createDate(); |
296 |
setDcCommon(dc, id, value); |
297 |
if (event != null) { |
298 |
dc.setEvent(event); |
299 |
} |
300 |
opfPackage.getMetadata().getDates().add(dc); |
301 |
return dc; |
302 |
} |
303 |
|
304 |
/** |
305 |
* Adds a new description to the publication. |
306 |
* |
307 |
* @param id |
308 |
* identifier or <code>null</code> |
309 |
* @param lang |
310 |
* the language code or <code>null</code> |
311 |
* @param value |
312 |
* the description text |
313 |
* @return the new description |
314 |
*/ |
315 |
public Description addDescription(String id, Locale lang, String value) { |
316 |
if (value == null) { |
317 |
throw new IllegalArgumentException("A value must be specified"); |
318 |
} |
319 |
Description dc = DCFactory.eINSTANCE.createDescription(); |
320 |
setDcLocalized(dc, id, lang, value); |
321 |
opfPackage.getMetadata().getDescriptions().add(dc); |
322 |
return dc; |
323 |
} |
324 |
|
325 |
/** |
326 |
* Adds an optional publication format. |
327 |
* |
328 |
* @param id |
329 |
* identifier or <code>null</code> |
330 |
* @param value |
331 |
* the format to add |
332 |
* @return the new format |
333 |
*/ |
334 |
public Format addFormat(String id, String value) { |
335 |
Format dc = DCFactory.eINSTANCE.createFormat(); |
336 |
setDcCommon(dc, id, value); |
337 |
opfPackage.getMetadata().getFormats().add(dc); |
338 |
return dc; |
339 |
} |
340 |
|
341 |
/** |
342 |
* Adds a new identifier to the publication. |
343 |
* |
344 |
* @param id |
345 |
* the identifier id |
346 |
* @param scheme |
347 |
* the scheme used for representing the identifier |
348 |
* @param value |
349 |
* the identifier value |
350 |
* @return the new identifier |
351 |
*/ |
352 |
public Identifier addIdentifier(String id, String scheme, String value) { |
353 |
if (value == null) { |
354 |
throw new IllegalArgumentException("A value must be specified"); |
355 |
} |
356 |
Identifier dc = DCFactory.eINSTANCE.createIdentifier(); |
357 |
dc.setId(id); |
358 |
dc.setScheme(scheme); |
359 |
FeatureMapUtil.addText(dc.getMixed(), value); |
360 |
opfPackage.getMetadata().getIdentifiers().add(dc); |
361 |
return dc; |
362 |
} |
363 |
|
364 |
/** |
365 |
* Adds a new item to the manifest using default values for properties not |
366 |
* specified. Same as |
367 |
* <code>addItem(null, null, file, null, null, true, false);</code>. |
368 |
* |
369 |
* @param file |
370 |
* @return the new item |
371 |
*/ |
372 |
public Item addItem(File file) { |
373 |
return addItem(null, null, file, null, null, true, true, false); |
374 |
} |
375 |
|
376 |
/** |
377 |
* Adds a new item to the manifest. If an identifier is not specified it |
378 |
* will automatically be assigned. |
379 |
* |
380 |
* <p> |
381 |
* The <i>spine</i> defines the reading order, so the order items are added |
382 |
* and whether or not <i>spine</i> is <code>true</code> does matter. Unless |
383 |
* a table of contents file has been specified it will be generated. All |
384 |
* files that have been added to the spine will be examined unless the |
385 |
* <i>noToc</i> attribute has been set to <code>true</code>. |
386 |
* </p> |
387 |
* |
388 |
* @param file |
389 |
* the file to add |
390 |
* @param dest |
391 |
* the destination sub-folder or <code>null</code> |
392 |
* @param id |
393 |
* identifier or <code>null</code> |
394 |
* @param type |
395 |
* MIME file type |
396 |
* @param spine |
397 |
* whether or not to add the item to the spine |
398 |
* @param linear |
399 |
* whether or not the item is part of the reading order |
400 |
* @param noToc |
401 |
* whether or not to include in TOC when automatically generated |
402 |
* @return the new item |
403 |
*/ |
404 |
public Item addItem(String id, Locale lang, File file, String dest, String type, boolean spine, boolean linear, |
405 |
boolean noToc) { |
406 |
if (file == null || !file.exists()) { |
407 |
throw new IllegalArgumentException("\"file\" " + file.getAbsolutePath() + " must exist."); |
408 |
} |
409 |
if (file.isDirectory()) { |
410 |
throw new IllegalArgumentException("\"file\" " + file.getAbsolutePath() + " must not be a directory."); |
411 |
} |
412 |
Item item = OPFFactory.eINSTANCE.createItem(); |
413 |
if (type == null) { |
414 |
type = EPUBFileUtil.getMimeType(file); |
415 |
if (type == null) { |
416 |
throw new IllegalArgumentException("Could not automatically determine MIME type for file " + file |
417 |
+ ". Please specify the correct value"); |
418 |
} |
419 |
} |
420 |
if (id == null) { |
421 |
String prefix = ""; |
422 |
if (!type.equals(MIMETYPE_XHTML)) { |
423 |
prefix = (type.indexOf('/')) == -1 ? type : type.substring(0, type.indexOf('/')) + "-"; |
424 |
} |
425 |
id = prefix + file.getName().substring(0, file.getName().lastIndexOf('.')); |
426 |
} |
427 |
item.setId(id); |
428 |
if (dest == null) { |
429 |
item.setHref(file.getName()); |
430 |
} else { |
431 |
item.setHref(dest + '/' + file.getName()); |
432 |
} |
433 |
item.setNoToc(noToc); |
434 |
item.setMedia_type(type); |
435 |
item.setFile(file.getAbsolutePath()); |
436 |
opfPackage.getManifest().getItems().add(item); |
437 |
if (spine) { |
438 |
Itemref ref = OPFFactory.eINSTANCE.createItemref(); |
439 |
if (!linear) { |
440 |
ref.setLinear("no"); |
441 |
} |
442 |
ref.setIdref(id); |
443 |
getSpine().getSpineItems().add(ref); |
444 |
} |
445 |
return item; |
446 |
} |
447 |
|
448 |
/** |
449 |
* Adds a new language specification to the publication. |
450 |
* |
451 |
* @param id |
452 |
* identifier or <code>null</code> |
453 |
* @param lang |
454 |
* the language code or <code>null</code> |
455 |
* @param lang |
456 |
* the RFC-3066 format of the language code |
457 |
* @return the language instance |
458 |
*/ |
459 |
public Language addLanguage(String id, String value) { |
460 |
if (value == null) { |
461 |
throw new IllegalArgumentException("A value must be specified"); |
462 |
} |
463 |
Language dc = DCFactory.eINSTANCE.createLanguage(); |
464 |
setDcCommon(dc, id, value); |
465 |
opfPackage.getMetadata().getLanguages().add(dc); |
466 |
return dc; |
467 |
} |
468 |
|
469 |
/** |
470 |
* Adds a new meta item to the publication. |
471 |
* |
472 |
* @param name |
473 |
* name of the item |
474 |
* @param value |
475 |
* content of the item |
476 |
* @return the new meta |
477 |
*/ |
478 |
public org.eclipse.mylyn.docs.epub.opf.Meta addMeta(String name, String value) { |
479 |
if (value == null) { |
480 |
throw new IllegalArgumentException("A value must be specified"); |
481 |
} |
482 |
if (name == null) { |
483 |
throw new IllegalArgumentException("A name must be specified"); |
484 |
} |
485 |
org.eclipse.mylyn.docs.epub.opf.Meta opf = OPFFactory.eINSTANCE.createMeta(); |
486 |
opf.setName(name); |
487 |
opf.setContent(value); |
488 |
opfPackage.getMetadata().getMetas().add(opf); |
489 |
return opf; |
490 |
} |
491 |
|
492 |
/** |
493 |
* Adds a new publisher to the publication. |
494 |
* |
495 |
* @param id |
496 |
* identifier or <code>null</code> |
497 |
* @param lang |
498 |
* the language code or <code>null</code> |
499 |
* @param value |
500 |
* name of the publisher |
501 |
* @return the new publisher |
502 |
*/ |
503 |
public Publisher addPublisher(String id, Locale lang, String value) { |
504 |
if (value == null) { |
505 |
throw new IllegalArgumentException("A value must be specified"); |
506 |
} |
507 |
Publisher dc = DCFactory.eINSTANCE.createPublisher(); |
508 |
setDcLocalized(dc, id, lang, value); |
509 |
opfPackage.getMetadata().getPublishers().add(dc); |
510 |
return dc; |
511 |
} |
512 |
|
513 |
/** |
514 |
* The structural components of the books are listed in reference elements |
515 |
* contained within the guide element. These components could refer to the |
516 |
* table of contents, list of illustrations, foreword, bibliography, and |
517 |
* many other standard parts of the book. Reading systems are not required |
518 |
* to use the guide element but it is a good idea to use it. |
519 |
* |
520 |
* @param href |
521 |
* the item referenced |
522 |
* @param title |
523 |
* title of the reference |
524 |
* @param value |
525 |
* type of the reference |
526 |
* @return the reference |
527 |
*/ |
528 |
public Reference addReference(String href, String title, Type value) { |
529 |
if (value == null) { |
530 |
throw new IllegalArgumentException("A value must be specified"); |
531 |
} |
532 |
if (href == null) { |
533 |
throw new IllegalArgumentException("A href must be specified"); |
534 |
} |
535 |
if (title == null) { |
536 |
throw new IllegalArgumentException("A title must be specified"); |
537 |
} |
538 |
Reference reference = OPFFactory.eINSTANCE.createReference(); |
539 |
reference.setHref(href); |
540 |
reference.setTitle(title); |
541 |
reference.setType(value); |
542 |
opfPackage.getGuide().getGuideItems().add(reference); |
543 |
return reference; |
544 |
} |
545 |
|
546 |
/** |
547 |
* Adds a optional <i>relation</i> specification to the publication. |
548 |
* |
549 |
* @param id |
550 |
* identifier or <code>null</code> |
551 |
* @param lang |
552 |
* the language code or <code>null</code> |
553 |
* @param value |
554 |
* the value of the relation |
555 |
* @return the new relation |
556 |
*/ |
557 |
public Relation addRelation(String id, Locale lang, String value) { |
558 |
if (value == null) { |
559 |
throw new IllegalArgumentException("A value must be specified"); |
560 |
} |
561 |
Relation dc = DCFactory.eINSTANCE.createRelation(); |
562 |
setDcLocalized(dc, id, lang, value); |
563 |
opfPackage.getMetadata().getRelations().add(dc); |
564 |
return dc; |
565 |
} |
566 |
|
567 |
/** |
568 |
* Adds a optional <i>rights</i> specification to the publication. |
569 |
* |
570 |
* @param id |
571 |
* identifier or <code>null</code> |
572 |
* @param lang |
573 |
* the language code or <code>null</code> |
574 |
* @param value |
575 |
* the rights text |
576 |
* @return the new rights element |
577 |
*/ |
578 |
public Rights addRights(String id, Locale lang, String value) { |
579 |
if (value == null) { |
580 |
throw new IllegalArgumentException("A value must be specified"); |
581 |
} |
582 |
Rights dc = DCFactory.eINSTANCE.createRights(); |
583 |
setDcLocalized(dc, id, lang, value); |
584 |
opfPackage.getMetadata().getRights().add(dc); |
585 |
return dc; |
586 |
} |
587 |
|
588 |
/** |
589 |
* Adds a optional <i>source</i> specification to the publication. |
590 |
* |
591 |
* @param id |
592 |
* identifier or <code>null</code> |
593 |
* @param lang |
594 |
* the language code or <code>null</code> |
595 |
* @param value |
596 |
* the source text |
597 |
* @return the new source element |
598 |
*/ |
599 |
public Source addSource(String id, Locale lang, String value) { |
600 |
if (value == null) { |
601 |
throw new IllegalArgumentException("A value must be specified"); |
602 |
} |
603 |
Source dc = DCFactory.eINSTANCE.createSource(); |
604 |
setDcLocalized(dc, id, lang, value); |
605 |
opfPackage.getMetadata().getSources().add(dc); |
606 |
return dc; |
607 |
} |
608 |
|
609 |
/** |
610 |
* Adds a required <i>subject</i> specification to the publication. |
611 |
* |
612 |
* @param id |
613 |
* identifier or <code>null</code> |
614 |
* @param lang |
615 |
* the language code or <code>null</code> |
616 |
* @param value |
617 |
* the subject |
618 |
*/ |
619 |
public Subject addSubject(String id, Locale lang, String value) { |
620 |
if (value == null) { |
621 |
throw new IllegalArgumentException("A value must be specified"); |
622 |
} |
623 |
Subject dc = DCFactory.eINSTANCE.createSubject(); |
624 |
setDcLocalized(dc, id, lang, value); |
625 |
opfPackage.getMetadata().getSubjects().add(dc); |
626 |
return dc; |
627 |
} |
628 |
|
629 |
/** |
630 |
* Adds a required <i>title</i> specification to the publication. |
631 |
* |
632 |
* @param id |
633 |
* identifier or <code>null</code> |
634 |
* @param lang |
635 |
* the language code or <code>null</code> |
636 |
* @param value |
637 |
* the new title |
638 |
* @return the new title |
639 |
*/ |
640 |
public Title addTitle(String id, Locale lang, String value) { |
641 |
if (value == null) { |
642 |
throw new IllegalArgumentException("A value must be specified"); |
643 |
} |
644 |
Title dc = DCFactory.eINSTANCE.createTitle(); |
645 |
setDcLocalized(dc, id, lang, value); |
646 |
opfPackage.getMetadata().getTitles().add(dc); |
647 |
return dc; |
648 |
} |
649 |
|
650 |
/** |
651 |
* Adds a optional <i>type</i> specification to the publication. |
652 |
* |
653 |
* @param id |
654 |
* identifier or <code>null</code> |
655 |
* @param type |
656 |
* the type to add |
657 |
* @return the new type |
658 |
*/ |
659 |
public org.eclipse.mylyn.docs.epub.dc.Type addType(String id, String value) { |
660 |
if (value == null) { |
661 |
throw new IllegalArgumentException("A value must be specified"); |
662 |
} |
663 |
org.eclipse.mylyn.docs.epub.dc.Type dc = DCFactory.eINSTANCE.createType(); |
664 |
setDcCommon(dc, id, value); |
665 |
opfPackage.getMetadata().getTypes().add(dc); |
666 |
return dc; |
667 |
} |
668 |
|
669 |
/** |
670 |
* Copies all items part of the publication into the OEPBS folder unless the |
671 |
* item in question will be generated. |
672 |
* |
673 |
* @param rootFolder |
674 |
* the folder to copy into. |
675 |
* @throws IOException |
676 |
*/ |
677 |
private void copyContent(File rootFolder) throws IOException { |
678 |
EList<Item> items = opfPackage.getManifest().getItems(); |
679 |
for (Item item : items) { |
680 |
if (!item.isGenerated()) { |
681 |
File source = new File(item.getFile()); |
682 |
File destination = new File(rootFolder.getAbsolutePath() + File.separator + item.getHref()); |
683 |
EPUBFileUtil.copy(source, destination); |
684 |
} |
685 |
} |
686 |
} |
687 |
|
688 |
/** |
689 |
* Implement to handle generation of table of contents from the items added |
690 |
* to the <i>spine</i>. |
691 |
* |
692 |
* @throws Exception |
693 |
*/ |
694 |
protected abstract void generateTableOfContents() throws Exception; |
695 |
|
696 |
/** |
697 |
* Returns the main identifier of the publication or <code>null</code> if it |
698 |
* could not be determined. |
699 |
* |
700 |
* @return the main identifier or <code>null</code> |
701 |
*/ |
702 |
protected Identifier getIdentifier() { |
703 |
EList<Identifier> identifiers = opfPackage.getMetadata().getIdentifiers(); |
704 |
for (Identifier identifier : identifiers) { |
705 |
if (identifier.getId().equals(opfPackage.getUniqueIdentifier())) { |
706 |
return identifier; |
707 |
} |
708 |
} |
709 |
return null; |
710 |
} |
711 |
|
712 |
/** |
713 |
* Locates and returns an item from the manifest corresponding to the given |
714 |
* identifier. |
715 |
* |
716 |
* @param id |
717 |
* the identifier |
718 |
* @return the item |
719 |
*/ |
720 |
public Item getItemById(String id) { |
721 |
EList<Item> items = opfPackage.getManifest().getItems(); |
722 |
for (Item item : items) { |
723 |
if (item.getId().equals(id)) { |
724 |
return item; |
725 |
} |
726 |
} |
727 |
return null; |
728 |
} |
729 |
|
730 |
/** |
731 |
* Returns a list of all manifest items that have the specified MIME type. |
732 |
* |
733 |
* @param mimetype |
734 |
* the MIME type to search for |
735 |
* @return a list of all items |
736 |
*/ |
737 |
public List<Item> getItemsByMIMEType(String mimetype) { |
738 |
ArrayList<Item> stylesheets = new ArrayList<Item>(); |
739 |
EList<Item> items = opfPackage.getManifest().getItems(); |
740 |
for (Item item : items) { |
741 |
if (item.getMedia_type().equals(mimetype)) { |
742 |
stylesheets.add(item); |
743 |
} |
744 |
} |
745 |
return stylesheets; |
746 |
} |
747 |
|
748 |
public Package getOpfPackage() { |
749 |
return opfPackage; |
750 |
} |
751 |
|
752 |
/** |
753 |
* Returns the root folder of the publication. This is the folder where the |
754 |
* OPF file resides. Note that this property will only have a value if this |
755 |
* instance has been populated using an existing publication, such as when |
756 |
* unpacking an EPUB file. |
757 |
* |
758 |
* @return the root folder or <code>null</code> |
759 |
*/ |
760 |
public File getRootFolder() { |
761 |
return rootFolder; |
762 |
} |
763 |
|
764 |
/** |
765 |
* Returns the publication spine. |
766 |
* |
767 |
* @return the spine |
768 |
*/ |
769 |
protected Spine getSpine() { |
770 |
return opfPackage.getSpine(); |
771 |
} |
772 |
|
773 |
/** |
774 |
* Returns the table of contents for the publication. As the actual |
775 |
* implementation may vary depending on |
776 |
* |
777 |
* @return the table of contents |
778 |
*/ |
779 |
public abstract Object getTableOfContents(); |
780 |
|
781 |
/** |
782 |
* Returns a list of validation messages. This list is only populated when |
783 |
* {@link #pack(File)} has taken place. |
784 |
* |
785 |
* @return a list of validation messages |
786 |
*/ |
787 |
public List<ValidationMessage> getValidationMessages() { |
788 |
return messages; |
789 |
} |
790 |
|
791 |
/** |
792 |
* Iterates over all files in the manifest attempting to determine |
793 |
* referenced resources such as image files and adds these to the manifest. |
794 |
* |
795 |
* @throws ParserConfigurationException |
796 |
* @throws SAXException |
797 |
* @throws IOException |
798 |
*/ |
799 |
private void includeReferencedResources() throws ParserConfigurationException, SAXException, IOException { |
800 |
EList<Item> manifestItems = opfPackage.getManifest().getItems(); |
801 |
// Compose a list of file references |
802 |
HashMap<File, List<File>> references = new HashMap<File, List<File>>(); |
803 |
for (Item item : manifestItems) { |
804 |
// Only parse XHTML-files and files that are not generated |
805 |
if (item.getMedia_type().equals(MIMETYPE_XHTML) && !item.isGenerated()) { |
806 |
if (item.getSourcePath() != null) { |
807 |
File source = new File(item.getSourcePath()); |
808 |
references.put(source, ReferenceScanner.parse(item)); |
809 |
} else { |
810 |
File source = new File(item.getFile()); |
811 |
references.put(source, ReferenceScanner.parse(item)); |
812 |
} |
813 |
} |
814 |
} |
815 |
for (File root : references.keySet()) { |
816 |
List<File> images = references.get(root); |
817 |
for (File image : images) { |
818 |
File relativePath = new File(EPUBFileUtil.getRelativePath(root, image)); |
819 |
addItem(null, null, image, relativePath.getParent(), null, false, false, false); |
820 |
} |
821 |
} |
822 |
|
823 |
} |
824 |
|
825 |
/** |
826 |
* Assembles the OPS publication in a location relative to the root file. |
827 |
* |
828 |
* @param rootFile |
829 |
* the root file |
830 |
* @throws Exception |
831 |
*/ |
832 |
void pack(File rootFile) throws Exception { |
833 |
if (opfPackage.getSpine().getSpineItems().isEmpty()) { |
834 |
throw new IllegalArgumentException("Spine does not contain any items"); |
835 |
} |
836 |
// Note that order is important here. Some of the steps for assembling |
837 |
// the EPUB may insert data into the EPUB structure. Hence the OPF must |
838 |
// be written last. |
839 |
this.rootFolder = rootFile.getAbsoluteFile().getParentFile(); |
840 |
addCompulsoryData(); |
841 |
if (rootFolder.isDirectory() || rootFolder.mkdirs()) { |
842 |
if (opfPackage.isGenerateCoverHTML()) { |
843 |
writeCoverHTML(rootFolder); |
844 |
} |
845 |
if (opfPackage.isIncludeReferencedResources()) { |
846 |
includeReferencedResources(); |
847 |
} |
848 |
copyContent(rootFolder); |
849 |
messages = validateContents(); |
850 |
writeTableOfContents(rootFolder); |
851 |
writeOPF(rootFile); |
852 |
} else { |
853 |
throw new IOException("Could not create OEBPS folder in " + rootFolder.getAbsolutePath()); |
854 |
} |
855 |
validateMetadata(); |
856 |
} |
857 |
|
858 |
/** |
859 |
* Reads the root file. |
860 |
* |
861 |
* @param rootFile |
862 |
* the file to read |
863 |
* @throws IOException |
864 |
*/ |
865 |
private void readOPF(File rootFile) throws IOException { |
866 |
ResourceSet resourceSet = new ResourceSetImpl(); |
867 |
URI fileURI = URI.createFileURI(rootFile.getAbsolutePath()); |
868 |
Resource resource = resourceSet.createResource(fileURI); |
869 |
resource.load(null); |
870 |
opfPackage = (Package) resource.getContents().get(0); |
871 |
} |
872 |
|
873 |
/** |
874 |
* Implement to read the table of contents for the particular OEPBS |
875 |
* implementation. |
876 |
* |
877 |
* @param tocFile |
878 |
* the table of contents file |
879 |
* @throws IOException |
880 |
*/ |
881 |
protected abstract void readTableOfContents(File tocFile) throws IOException; |
882 |
|
883 |
/** |
884 |
* Registers a new resource factory for OPF data structures. This is |
885 |
* normally done through Eclipse extension points but we also need to be |
886 |
* able to create this factory without the Eclipse runtime. |
887 |
*/ |
888 |
private void registerOPFResourceFactory() { |
889 |
// Register package so that it is available even without the Eclipse |
890 |
// runtime |
891 |
@SuppressWarnings("unused") |
892 |
OPFPackage packageInstance = OPFPackage.eINSTANCE; |
893 |
|
894 |
// Register the file suffix |
895 |
Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put(OPF_FILE_SUFFIX, |
896 |
new XMLResourceFactoryImpl() { |
897 |
|
898 |
@Override |
899 |
public Resource createResource(URI uri) { |
900 |
OPFResourceImpl xmiResource = new OPFResourceImpl(uri) { |
901 |
|
902 |
@Override |
903 |
protected XMLHelper createXMLHelper() { |
904 |
EPUBXMLHelperImp xmlHelper = new EPUBXMLHelperImp(); |
905 |
return xmlHelper; |
906 |
} |
907 |
|
908 |
}; |
909 |
Map<Object, Object> loadOptions = xmiResource.getDefaultLoadOptions(); |
910 |
Map<Object, Object> saveOptions = xmiResource.getDefaultSaveOptions(); |
911 |
// We use extended metadata |
912 |
saveOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, Boolean.TRUE); |
913 |
loadOptions.put(XMLResource.OPTION_EXTENDED_META_DATA, Boolean.TRUE); |
914 |
// Required in order to correctly read in attributes |
915 |
loadOptions.put(XMLResource.OPTION_LAX_FEATURE_PROCESSING, Boolean.TRUE); |
916 |
// Treat "href" attributes as features |
917 |
loadOptions.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE); |
918 |
// UTF-8 encoding is required per specification |
919 |
saveOptions.put(XMLResource.OPTION_ENCODING, XML_ENCODING); |
920 |
return xmiResource; |
921 |
} |
922 |
|
923 |
}); |
924 |
} |
925 |
|
926 |
/** |
927 |
* Convenience method for adding a cover to the publication. This method |
928 |
* will make sure the required actions are taken to provide a cover page for |
929 |
* all reading systems. |
930 |
* |
931 |
* @param image |
932 |
* the cover image (jpeg, png, svg or gif) |
933 |
* @param title |
934 |
* title of the cover page |
935 |
*/ |
936 |
public void setCover(File image, String title) { |
937 |
// Add the cover image to the manifest |
938 |
Item item = addItem(COVER_IMAGE_ID, null, image, null, null, false, false, true); |
939 |
item.setTitle(title); |
940 |
// Point to the cover using a meta tag |
941 |
addMeta(COVER_ID, COVER_IMAGE_ID); |
942 |
opfPackage.setGenerateCoverHTML(true); |
943 |
|
944 |
} |
945 |
|
946 |
/** |
947 |
* Sets common properties for <i>Dublin Core</i> elements. |
948 |
* |
949 |
* @param dc |
950 |
* the Dublin Core element |
951 |
* @param id |
952 |
* optional identifier |
953 |
* @param value |
954 |
* value of the element |
955 |
*/ |
956 |
private void setDcCommon(DCType dc, String id, String value) { |
957 |
FeatureMapUtil.addText(dc.getMixed(), value); |
958 |
if (id != null) { |
959 |
dc.setId(id); |
960 |
} |
961 |
} |
962 |
|
963 |
/** |
964 |
* Sets common properties for localized <i>Dublin Core</i> elements. |
965 |
* |
966 |
* @param dc |
967 |
* the Dublin Core element |
968 |
* @param id |
969 |
* optional identifier |
970 |
* @param lang |
971 |
* language code |
972 |
* @param value |
973 |
* value of the element |
974 |
*/ |
975 |
private void setDcLocalized(LocalizedDCType dc, String id, Locale lang, String value) { |
976 |
setDcCommon(dc, id, value); |
977 |
if (lang != null) { |
978 |
dc.setLang(lang.toString()); |
979 |
} |
980 |
} |
981 |
|
982 |
/** |
983 |
* Specifies whether or not to automatically generate table of contents from |
984 |
* the publication contents. The default is <code>true</code> |
985 |
* |
986 |
* @param generateToc |
987 |
* whether or not to generate a table of contents |
988 |
*/ |
989 |
public void setGenerateToc(boolean generateToc) { |
990 |
opfPackage.setGenerateTableOfContents(generateToc); |
991 |
} |
992 |
|
993 |
/** |
994 |
* Specifies the id of the identifier used for the publication. |
995 |
* |
996 |
* @param identifier_id |
997 |
* the identifier id |
998 |
* @see #addIdentifier(String, String, String) |
999 |
*/ |
1000 |
public void setIdentifierId(String identifier_id) { |
1001 |
opfPackage.setUniqueIdentifier(identifier_id); |
1002 |
} |
1003 |
|
1004 |
/** |
1005 |
* Specifies whether or not to automatically include resources (files) that |
1006 |
* are referenced in the contents. The default is <code>false</code>. |
1007 |
* |
1008 |
* @param include |
1009 |
* whether or not automatically include resources |
1010 |
*/ |
1011 |
public void setIncludeReferencedResources(boolean include) { |
1012 |
opfPackage.setIncludeReferencedResources(include); |
1013 |
} |
1014 |
|
1015 |
/** |
1016 |
* Specifies a target of contents file for the publication. This is an |
1017 |
* alternative to {@link #setGenerateToc(boolean)}. |
1018 |
* |
1019 |
* @param tocFile |
1020 |
* the table of contents file |
1021 |
*/ |
1022 |
public abstract void setTableOfContents(File tocFile); |
1023 |
|
1024 |
/** |
1025 |
* Populates the data model with the content from an unpacked EPUB. |
1026 |
* |
1027 |
* @param epubFile |
1028 |
* the EPUB file to unpack |
1029 |
* @param destination |
1030 |
* the destination folder |
1031 |
* @throws Exception |
1032 |
*/ |
1033 |
void unpack(File rootFile) throws Exception { |
1034 |
readOPF(rootFile); |
1035 |
rootFolder = rootFile.getAbsoluteFile().getParentFile(); |
1036 |
String tocId = opfPackage.getSpine().getToc(); |
1037 |
Item tocItem = getItemById(tocId); |
1038 |
File tocFile = new File(rootFolder.getAbsolutePath() + File.separator + tocItem.getHref()); |
1039 |
readTableOfContents(tocFile); |
1040 |
} |
1041 |
|
1042 |
/** |
1043 |
* Implement to validate contents. |
1044 |
* |
1045 |
* @throws Exception |
1046 |
*/ |
1047 |
protected abstract List<ValidationMessage> validateContents() throws Exception; |
1048 |
|
1049 |
/** |
1050 |
* Validates the data model contents. |
1051 |
* |
1052 |
* @return a list of EMF diagnostics |
1053 |
*/ |
1054 |
public List<Diagnostic> validateMetadata() { |
1055 |
EValidator.Registry.INSTANCE.put(OPFPackage.eINSTANCE, new EcoreValidator()); |
1056 |
BasicDiagnostic diagnostics = new BasicDiagnostic(); |
1057 |
for (EObject eo : opfPackage.eContents()) { |
1058 |
Map<Object, Object> context = new HashMap<Object, Object>(); |
1059 |
Diagnostician.INSTANCE.validate(eo, diagnostics, context); |
1060 |
} |
1061 |
return diagnostics.getChildren(); |
1062 |
} |
1063 |
|
1064 |
/** |
1065 |
* Writes a XHTML-file for the cover image. This is added to the publication |
1066 |
* and all required references set. |
1067 |
* |
1068 |
* @param rootFolder |
1069 |
* the publication root folder |
1070 |
* @throws IOException |
1071 |
* |
1072 |
*/ |
1073 |
private void writeCoverHTML(File rootFolder) throws IOException { |
1074 |
Item coverImage = getItemById(COVER_IMAGE_ID); |
1075 |
File coverFile = new File(rootFolder.getAbsolutePath() + File.separator + "cover-page.xhtml"); |
1076 |
if (!coverFile.exists()) { |
1077 |
|
1078 |
try { |
1079 |
FileWriter fw = new FileWriter(coverFile); |
1080 |
fw.append("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n"); |
1081 |
fw.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"); |
1082 |
fw.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"); |
1083 |
fw.append(" <head>\n"); |
1084 |
fw.append(" <title>" + coverImage.getTitle() + "</title>\n"); |
1085 |
fw.append(" <style type=\"text/css\">"); |
1086 |
fw.append(" #cover-body {\n"); |
1087 |
fw.append(" margin: 0px;\n"); |
1088 |
fw.append(" text-align: center;\n"); |
1089 |
fw.append(" background-color: #222222;\n"); |
1090 |
fw.append(" }\n"); |
1091 |
fw.append(" #cover-block {\n"); |
1092 |
fw.append(" height: 100%;\n"); |
1093 |
fw.append(" margin-top: 0;\n"); |
1094 |
fw.append(" }\n"); |
1095 |
fw.append(" #cover-image {\n"); |
1096 |
fw.append(" height: 100%;\n"); |
1097 |
fw.append(" text-align: center;\n"); |
1098 |
fw.append(" max-width: 100%;\n"); |
1099 |
fw.append(" }\n"); |
1100 |
fw.append(" </style>\n"); |
1101 |
fw.append(" </head>\n"); |
1102 |
fw.append(" <body id=\"cover-body\">\n"); |
1103 |
fw.append(" <div id=\"cover-block\">\n"); |
1104 |
fw.append(" <img id=\"cover-image\" src=\"" + coverImage.getHref() + "\" alt=\"" |
1105 |
+ coverImage.getTitle() + "\"/>\n"); |
1106 |
fw.append(" </div>\n"); |
1107 |
fw.append(" </body>\n"); |
1108 |
fw.append("</html>\n"); |
1109 |
fw.close(); |
1110 |
} catch (IOException e) { |
1111 |
e.printStackTrace(); |
1112 |
} |
1113 |
} |
1114 |
// Add the cover page item |
1115 |
Item coverPage = addItem(COVER_ID, null, coverFile, null, MIMETYPE_XHTML, true, false, false); |
1116 |
coverPage.setGenerated(true); |
1117 |
addReference(coverPage.getHref(), coverImage.getTitle(), Type.COVER); |
1118 |
// Move the cover page first in the spine. |
1119 |
EList<Itemref> spine = opfPackage.getSpine().getSpineItems(); |
1120 |
Itemref cover = null; |
1121 |
for (Itemref itemref : spine) { |
1122 |
if (itemref.getIdref().equals(COVER_ID)) { |
1123 |
cover = itemref; |
1124 |
} |
1125 |
} |
1126 |
if (cover != null) { |
1127 |
spine.move(0, cover); |
1128 |
} |
1129 |
} |
1130 |
|
1131 |
/** |
1132 |
* Writes the <b>content.opf</b> file. |
1133 |
* |
1134 |
* @param rootFolder |
1135 |
* the folder where to write the file. |
1136 |
* @throws IOException |
1137 |
*/ |
1138 |
private void writeOPF(File opfFile) throws IOException { |
1139 |
ResourceSet resourceSet = new ResourceSetImpl(); |
1140 |
// Register the packages to make it available during loading. |
1141 |
URI fileURI = URI.createFileURI(opfFile.getAbsolutePath()); |
1142 |
Resource resource = resourceSet.createResource(fileURI); |
1143 |
resource.getContents().add(opfPackage); |
1144 |
resource.save(null); |
1145 |
} |
1146 |
|
1147 |
/** |
1148 |
* Implement to handle writing of the table of contents. Note that this |
1149 |
* method should do nothing if the table of contents has already been |
1150 |
* specified using {@link #setTableOfContents(File)}. |
1151 |
* |
1152 |
* @param rootFolder |
1153 |
* the folder to write in |
1154 |
* @throws Exception |
1155 |
*/ |
1156 |
protected abstract void writeTableOfContents(File rootFolder) throws Exception; |
1157 |
} |