View Javadoc
1   /*===========================================================================
2     Copyright (C) 2009-2011 by the Okapi Framework contributors
3   -----------------------------------------------------------------------------
4     Licensed under the Apache License, Version 2.0 (the "License");
5     you may not use this file except in compliance with the License.
6     You may obtain a copy of the License at
7   
8     http://www.apache.org/licenses/LICENSE-2.0
9   
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15  ===========================================================================*/
16  
17  package net.sf.okapi.filters.openxml;
18  
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileNotFoundException;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.charset.StandardCharsets;
27  import java.util.LinkedHashMap;
28  import java.util.Stack;
29  import java.util.TreeMap;
30  import java.util.zip.ZipEntry;
31  import java.util.zip.ZipFile;
32  import java.util.zip.ZipOutputStream;
33  
34  import javax.xml.stream.XMLEventFactory;
35  import javax.xml.stream.XMLInputFactory;
36  import javax.xml.stream.XMLOutputFactory;
37  import javax.xml.stream.XMLStreamException;
38  
39  import com.acumenvelocity.ath.common.Log;
40  
41  import net.sf.okapi.common.DefaultLocalePair;
42  import net.sf.okapi.common.Event;
43  import net.sf.okapi.common.EventType;
44  import net.sf.okapi.common.IParameters;
45  import net.sf.okapi.common.ISkeleton;
46  import net.sf.okapi.common.LocaleId;
47  import net.sf.okapi.common.Util;
48  import net.sf.okapi.common.encoder.EncoderManager;
49  import net.sf.okapi.common.exceptions.OkapiFileNotFoundException;
50  import net.sf.okapi.common.exceptions.OkapiIOException;
51  import net.sf.okapi.common.filters.fontmappings.FontMappings;
52  import net.sf.okapi.common.filterwriter.GenericFilterWriter;
53  import net.sf.okapi.common.filterwriter.IFilterWriter;
54  import net.sf.okapi.common.resource.DocumentPart;
55  import net.sf.okapi.common.resource.Ending;
56  import net.sf.okapi.common.resource.ITextUnit;
57  import net.sf.okapi.common.resource.Property;
58  import net.sf.okapi.common.resource.StartDocument;
59  import net.sf.okapi.common.resource.StartSubDocument;
60  import net.sf.okapi.common.resource.TextContainer;
61  import net.sf.okapi.common.resource.TextFragment;
62  import net.sf.okapi.common.skeleton.GenericSkeleton;
63  import net.sf.okapi.common.skeleton.GenericSkeletonWriter;
64  import net.sf.okapi.common.skeleton.ISkeletonWriter;
65  import net.sf.okapi.common.skeleton.ZipSkeleton;
66  
67  /**
68   * <p>Implements the IFilterWriter interface for the OpenXMLFilter, which
69   * filters Microsoft Office Word, Excel, and Powerpoint Documents. OpenXML 
70   * is the format of these documents.
71   * 
72   * <p>Since OpenXML files are Zip files that contain XML documents,
73   * this filter writer handles writing out the zip file, and
74   * uses OpenXMLContentSkeletonWriter to output the XML documents.
75   * 
76   * @version okapi v1.46.0
77   * SV: warning instead of an exception in hangleEvent()
78   * SV: added the default clause in handleEvent()
79   */
80  
81  public class OpenXMLFilterWriter implements IFilterWriter {
82  	private ConditionalParameters cparams;
83  	private final XMLInputFactory inputFactory;
84  	private final XMLOutputFactory outputFactory;
85  	private final XMLEventFactory eventFactory;
86  	private final DispersedTranslations dispersedTranslations;
87  
88  	private String outputPath;
89  	private Document.General document;
90  	private ZipOutputStream zipOut;
91  	private byte[] buffer;
92  	private LocaleId sourceLocale;
93  	private LocaleId targetLocale;
94  	private FontMappings fontMappings;
95  	private File tempFile;
96  	private File tempZip;
97  
98  	private EncoderManager encoderManager;
99  	private ZipEntry subDocEntry;
100 	private IFilterWriter subDocWriter;
101 	private ISkeletonWriter subSkelWriter;
102 	private final TreeMap<Integer, SubDocumentValues> tmSubDoc = new TreeMap<>();
103 	private int ndxSubDoc = 0;
104 	private OutputStream outputStream;
105 
106 	/**
107 	 * No-arg constructor in case it's needed.  Create local factory instances.
108 	 *
109 	 * Be aware - it is strongly encouraged to construct the filter writer with the primary
110 	 * constructor (another one) when an Excel document is going to be filtered, otherwise, there is
111 	 * no chance that the dispersed translations will be correctly initialised.
112 	 * Also, the XML input, output and event factories instantiation will not take into account
113 	 * the relevant class loader.
114 	 */
115 	public OpenXMLFilterWriter() {
116 		this(
117 			new ConditionalParameters(),
118 			XMLInputFactory.newInstance(),
119 			XMLOutputFactory.newInstance(),
120 			XMLEventFactory.newInstance(),
121 			new DispersedTranslations.Default()
122 		);
123 		OpenXMLFilter.configure(this.inputFactory, this.cparams);
124 	}
125 
126 	OpenXMLFilterWriter(
127 		final ConditionalParameters cparams,
128 		final XMLInputFactory inputFactory,
129 		final XMLOutputFactory outputFactory,
130 		final XMLEventFactory eventFactory,
131 		final DispersedTranslations dispersedTranslations
132 	) {
133 		this.cparams = cparams;
134 		this.inputFactory = inputFactory;
135 		this.outputFactory = outputFactory;
136 		this.eventFactory = eventFactory;
137 		this.dispersedTranslations = dispersedTranslations;
138 	}
139 
140 	/**
141 	 * Cancels processing of a filter; yet to be implemented.
142 	 */
143 	@Override
144     public void cancel () {
145 		//TODO: implement cancel()
146 	}
147 	
148 	/**
149 	 * Closes the zip file.
150 	 */
151 	@Override
152     public void close () {
153 		if ( zipOut == null ) return;
154 		IOException err = null;
155 		InputStream orig = null;
156 		OutputStream dest = null;
157 		try {
158 			// Closing reference to the original input stream 
159 			if (document != null){
160 				document.close();
161 				document = null;
162 			}
163 			
164 			// Close the output
165 			zipOut.close();
166 			zipOut = null;
167 
168 			// If it was in a temporary file, copy it over the existing one
169 			// If the IFilter.close() is called before IFilterWriter.close()
170 			// this should allow to overwrite the input.
171 			if ( tempZip != null ) {
172 				dest = new FileOutputStream(outputPath);
173 				orig = new FileInputStream(tempZip); 
174 				int len;
175 				while ( (len = orig.read(buffer)) > 0 ) {
176 					dest.write(buffer, 0, len);
177 				}
178 			}
179 			buffer = null;
180 		}
181 		catch ( IOException e ) {
182 			err = e;
183 		}
184 		finally {
185 			// Make sure we close both files
186 			if ( dest != null ) {
187 				try {
188 					dest.close();
189 				}
190 				catch ( IOException e ) {
191 					err = e;
192 				}
193 				dest = null;
194 			}
195 			if ( orig != null ) {
196 				try {
197 					orig.close();
198 				} catch ( IOException e ) {
199 					err = e;
200 				}
201 				orig = null;
202 				if ( err != null ) {
203 					throw new OkapiIOException("Error closing MS Office 2007 file.");
204 				} else {
205 					if ( tempZip != null ) {
206 						tempZip.delete();
207 						tempZip = null;
208 					}
209 				}
210 			}
211 		}
212 	}
213 
214 	/**
215 	 * Gets the name of the filter writer.
216 	 */
217 	@Override
218     public String getName () {
219 		return "OpenXMLZipFilterWriter"; 
220 	}
221 
222 	@Override
223     public EncoderManager getEncoderManager () {
224 		if ( encoderManager == null ) {
225 			encoderManager = new EncoderManager();
226 			encoderManager.setMapping(OpenXMLFilter.MIME_TYPE, "net.sf.okapi.common.encoder.XMLEncoder");
227 			encoderManager.setDefaultOptions(null, OpenXMLFilter.ENCODING.name(), OpenXMLFilter.LINE_BREAK);
228 		}
229 		return encoderManager;
230 	}
231 	
232 	@Override
233 	public ISkeletonWriter getSkeletonWriter () {
234 		return subSkelWriter;
235 	}
236 
237 	/**
238 	 * Handles an event.  Passes all but START_DOCUMENT, END_DOCUMENT,
239                * and DOCUMENT_PART to subdocument processing.
240 	 * @param event the event to process
241 	 */
242 	@Override
243 	public Event handleEvent (Event event) {
244     switch ( event.getEventType() ) {
245     case START_DOCUMENT:
246       processStartDocument((StartDocument)event.getResource());
247       break;
248     case DOCUMENT_PART:
249       processDocumentPart(event);
250       break;
251     case END_DOCUMENT:
252       processEndDocument();
253       break;
254     case START_SUBDOCUMENT:
255       processStartSubDocument((StartSubDocument)event.getResource());
256       break;
257     case END_SUBDOCUMENT:
258       processEndSubDocument((Ending)event.getResource());
259       break;
260     case TEXT_UNIT:
261       final ITextUnit textUnit = event.getTextUnit();
262       final Property sheetNameProperty = textUnit.getProperty(ExcelWorksheetTransUnitProperty.SHEET_NAME.getKeyName());
263       final Property cellReferenceProperty = textUnit.getProperty(ExcelWorksheetTransUnitProperty.CELL_REFERENCE.getKeyName());
264       if (null != sheetNameProperty && null != cellReferenceProperty) {
265         final CrossSheetCellReference crossSheetCellReference = new CrossSheetCellReference(
266           sheetNameProperty.getValue(),
267           new CellReference(cellReferenceProperty.getValue())
268         );
269         if (this.dispersedTranslations.presentFor(crossSheetCellReference)) {
270           final ITextUnit tu = event.getTextUnit().clone();
271           if (tu.hasTarget(this.targetLocale)) {
272             final TextContainer tc = tu.getTarget(this.targetLocale);
273             tc.setContent(new TextFragment(tc.getUnSegmentedContentCopy().getText()));
274             final GenericSkeleton skeleton = new GenericSkeleton();
275             tu.setSkeleton(skeleton);
276             skeleton.addContentPlaceholder(tu);
277             this.dispersedTranslations.update(
278               crossSheetCellReference,
279               this.subDocWriter.getSkeletonWriter().processTextUnit(tu)
280             );
281           }
282         }
283       }
284     case START_GROUP:
285     case END_GROUP:
286     case START_SUBFILTER:
287     case END_SUBFILTER:
288       try {
289         subDocWriter.handleEvent(event);
290       } catch(Throwable e) {
291         // String mess = e.getMessage();
292         // throw new OkapiNotImplementedException(mess, e); // kludge
293         Log.warn(getClass(), "Error processing {} '{}': {}", event.getEventType(),
294                   event.getResource().getId(), e.getMessage());
295       }
296       break;
297     case CANCELED:
298       break;
299                 default:
300                   break;
301     }
302     return event;
303   }
304 
305 	@Override
306     public void setOptions (LocaleId language,
307                             String defaultEncoding)
308 	{
309 		targetLocale = language;
310 		this.dispersedTranslations.prepareFor(this.targetLocale);
311 	}
312 
313 	@Override
314 	public void setOutput (String path) {
315 		outputPath = path;
316 	}
317 
318 	@Override
319 	public void setOutput (OutputStream output) {
320 		this.outputStream = output;
321 	}
322 
323 	/**
324 	 * Processes the start document for the whole zip file by
325                * initializing a temporary output file, and and output stream.
326 	 * @param res a resource for the start document
327 	 */
328 
329 	private void processStartDocument (StartDocument res) {
330 		try {
331 			buffer = new byte[2048];
332 			sourceLocale = res.getLocale();
333 			this.fontMappings = this.cparams.fontMappings().applicableTo(
334 				new DefaultLocalePair(this.sourceLocale, this.targetLocale)
335 			);
336 			ZipSkeleton skel = (ZipSkeleton)res.getSkeleton();
337 			ZipFile zipTemp = skel.getOriginal(); // if OpenXML filter was closed, this ZipFile has been marked for close
338 			File fZip = new File(zipTemp.getName()); // so get its name
339 			this.document = new Document.General(
340 					this.cparams,
341 					this.inputFactory,
342 					this.outputFactory,
343 					this.eventFactory,
344 					this.dispersedTranslations,
345 					res.getFilterId(),
346 					fZip.toURI(),
347 					this.sourceLocale,
348 					res.getEncoding(),
349 					null,
350 					null,
351 					null
352 			);
353 			document.open();
354               // *** this might not work if the ZipFile was from a URI that was not a normal file path ***
355 			tempZip = null;
356 			// Create the output stream from the path provided
357 			boolean useTemp = false;
358 			File f;
359 			OutputStream os;
360 			if (outputStream == null) {							
361 				f = new File(outputPath);
362 				if ( f.exists() ) {
363 					// If the file exists, try to remove
364 					useTemp = !f.delete();				
365 				}
366 				if (useTemp) {
367 					// Use a temporary output if we can overwrite for now
368 					// If it's the input file, IFilter.close() will free it before we
369 					// call close() here (that is if IFilter.close() is called correctly!)
370 					tempZip = File.createTempFile("~okapi-24_zfwTmpZip_", null);
371 					os = new FileOutputStream(tempZip.getAbsolutePath());
372 				} else {
373 					Util.createDirectories(outputPath);
374 					os = new FileOutputStream(outputPath);
375 				}
376 			} else {
377 				os = outputStream;
378 			}
379 			
380 			// create zip output
381 			zipOut = new ZipOutputStream(os);		
382 		}
383 		catch ( FileNotFoundException e ) {
384 			throw new OkapiFileNotFoundException("Existing file could not be overwritten.", e);
385 		}
386 		catch ( IOException | XMLStreamException e) {
387 			throw new OkapiIOException("File could not be written.", e);
388 		}
389 	}
390 	
391 	private void processEndDocument () {
392 		close();
393 	}
394 	
395 	/**
396 	 * This passes a file that doesn't need processing from the input zip file to the output zip file.
397 	 *
398 	 * @param event corresponding to the file to be passed through
399 	 */
400 	private void processDocumentPart (Event event) {
401 		DocumentPart documentPart = (DocumentPart) event.getResource();
402 
403 		if ( documentPart.getSkeleton() instanceof ZipSkeleton ) {
404 			ZipSkeleton skeleton = (ZipSkeleton) documentPart.getSkeleton();
405 			// Copy the entry data
406 			try {
407 				zipOut.putNextEntry(new ZipEntry(skeleton.getEntry().getName()));
408 				if (skeleton instanceof MarkupZipSkeleton) {
409 					clarify(((MarkupZipSkeleton) skeleton).markup());
410 					((MarkupZipSkeleton) skeleton).to(zipOut);
411 				} else {
412 					// If the contents were modified by the filter, write out the new data
413 					final String modifiedContents = skeleton.getModifiedContents();
414 					if (modifiedContents != null) {
415 						zipOut.write(modifiedContents.getBytes(StandardCharsets.UTF_8));
416 					} else {
417 						copyFrom(document.inputStreamFor(skeleton.getEntry()));
418 					}
419 				}
420 				zipOut.closeEntry();
421 			}
422 			catch (IOException | XMLStreamException e ) {
423 				throw new OkapiIOException("Error writing zip file entry.");
424 			}
425 		}
426 		else { // Otherwise it's a normal skeleton event
427 			subDocWriter.handleEvent(event);
428 		}
429 	}
430 
431 	private void clarify(final Markup markup) throws XMLStreamException, IOException {
432 		markup.apply(this.fontMappings);
433 		Nameable nameableMarkupComponent = markup.nameableComponent();
434 
435 		if (null != nameableMarkupComponent) {
436 			final MarkupClarificationConfiguration mcc = new MarkupClarificationConfiguration(
437 				this.cparams,
438 				this.eventFactory,
439                 this.document.presetColorValues(),
440 				this.document.highlightColorValues(),
441 				this.document.systemColorValues(),
442 				this.document.indexedColors(),
443 				this.document.mainPartTheme(),
444 				this.sourceLocale,
445 				this.targetLocale,
446 				this.dispersedTranslations
447 			);
448 			mcc.prepareFor(nameableMarkupComponent);
449 			new MarkupClarification(mcc).performFor(markup);
450 		}
451 	}
452 
453 	/**
454 	 * Starts processing a new file withing the zip file.  It looks for the 
455                * element type of "filetype" in the yaml parameters which need to
456                * be set before handleEvent is called, and need to be the same as
457                * the parameters on the START_SUBDOCUMENT event from the
458                * OpenXMLFilter (by calling setParameters).  Once the type of the
459                * file is discovered from the Parameters, a subdoc writer is 
460                * created from OpenXMLContentSkeletonWriter, and a temporary
461                * output file is created.
462 	 * @param res resource of the StartSubDocument
463 	 */
464 	private void processStartSubDocument (StartSubDocument res) {
465 		ndxSubDoc++; // DWH 1-10-2013 subDoc map
466 
467 		// Set the temporary path and create it
468 		try {
469 			tempFile = File.createTempFile("~okapi-25_zfwTmp"+ndxSubDoc+"_", null);
470 
471 			ISkeleton skel = res.getSkeleton();
472 			ConditionalParameters conditionalParameters = (ConditionalParameters) res.getFilterParameters();
473 			if (skel instanceof ZipSkeleton) {
474 				subDocEntry = ((ZipSkeleton) res.getSkeleton()).getEntry();
475 				if (document.isStyledTextPart(subDocEntry)) {
476 					subSkelWriter = new StyledTextSkeletonWriter(
477 						conditionalParameters,
478 						this.eventFactory,
479 						this.document.presetColorValues(),
480 						this.document.highlightColorValues(),
481 						this.document.systemColorValues(),
482 						this.document.indexedColors(),
483 						this.document.mainPartTheme(),
484 						this.sourceLocale,
485 						this.targetLocale,
486 						this.dispersedTranslations,
487 						this.fontMappings,
488 						this.document.styleDefinitionsFor(this.subDocEntry),
489 						innerGenericSkeletonWriter()
490 					);
491 				} else {
492 					subSkelWriter = new GenericSkeletonWriter();
493 				}
494 			} else {
495 				subDocEntry = new ZipEntry(res.getName());
496 				subSkelWriter = new GenericSkeletonWriter();
497 			}
498 		} catch (IOException | XMLStreamException e) {
499 			throw new OkapiIOException("Error opening temporary zip output file.");
500 		}
501 
502 		subDocWriter = new GenericFilterWriter(subSkelWriter, getEncoderManager()); // YS 12-20-09
503 		subDocWriter.setOptions(targetLocale, OpenXMLFilter.ENCODING.name());
504 		subDocWriter.setOutput(tempFile.getAbsolutePath());
505 		
506 		StartDocument sd = new StartDocument("sd");
507 		sd.setLineBreak(OpenXMLFilter.LINE_BREAK);
508 		sd.setSkeleton(res.getSkeleton());
509 		sd.setLocale(sourceLocale);
510 		subDocWriter.handleEvent(new Event(EventType.START_DOCUMENT, sd));
511 		SubDocumentValues subDocumentValues = new SubDocumentValues(
512 			subDocEntry,
513 			subDocWriter,
514 			subSkelWriter,
515 			tempFile
516 		);
517 		tmSubDoc.put(ndxSubDoc, subDocumentValues);
518 	}
519 
520 	private GenericSkeletonWriter innerGenericSkeletonWriter() {
521 		return new GenericSkeletonWriter(
522 				sourceLocale,
523 				targetLocale,
524 				getEncoderManager(),
525 				false,
526 				false,
527 				new LinkedHashMap<>(),
528 				new Stack<>(),
529 				OpenXMLFilter.ENCODING.name(),
530 				0,
531 				null
532 		);
533 	}
534 
535 	/**
536 	 * Finishes writing the subdocument temporary file, then adds it as an
537                * entry in the temporary zip output file.
538 	 * @param res resource of the end subdocument
539 	 */
540 	private void processEndSubDocument (Ending res) {
541 		try {
542 			SubDocumentValues subDocumentValues = tmSubDoc.get(ndxSubDoc--);
543 			subDocEntry = subDocumentValues.zipEntry;
544 			subDocWriter = subDocumentValues.filterWriter;
545 			subSkelWriter = subDocumentValues.skeletonWriter;
546 			tempFile = subDocumentValues.tempFile;
547 			// Finish writing the sub-document
548 			subDocWriter.handleEvent(new Event(EventType.END_DOCUMENT, res));
549 			subDocWriter.close();
550 
551 			// Create the new entry from the temporary output file
552 			zipOut.putNextEntry(new ZipEntry(subDocEntry.getName()));
553 			copyFrom(new FileInputStream(tempFile));
554 			zipOut.closeEntry();
555 			// Delete the temporary file
556 			tempFile.delete();
557 		}
558 		catch ( IOException e ) {
559 			throw new OkapiIOException("Error closing zip output file.");
560 		}
561 	}
562 
563 	private void copyFrom(final InputStream inputStream) throws IOException {
564 		int len;
565 		while ((len = inputStream.read(buffer)) > 0) {
566 			zipOut.write(buffer, 0, len);
567 		}
568 		inputStream.close();
569 	}
570 
571 	@Override
572     public void setParameters(IParameters params) // DWH 7-16-09
573 	{
574 		this.cparams = (ConditionalParameters)params;
575 	}
576 
577 	@Override
578 	public ConditionalParameters getParameters() // DWH 7-16-09
579 	{
580 		return cparams;
581 	}
582 
583 	private static class SubDocumentValues {
584 		private final ZipEntry zipEntry;
585 		private final IFilterWriter filterWriter;
586 		private final ISkeletonWriter skeletonWriter;
587 		private final File tempFile;
588 
589 		SubDocumentValues(
590 			final ZipEntry zipEntry,
591 			final IFilterWriter filterWriter,
592 			final ISkeletonWriter skeletonWriter,
593 			final File tempFile
594 		) {
595 			this.zipEntry = zipEntry;
596 			this.filterWriter = filterWriter;
597 			this.skeletonWriter = skeletonWriter;
598 			this.tempFile = tempFile;
599 		}
600 	}
601 }