View Javadoc
1   /*
2    * dpkg - Debian Package library and the Debian Package Maven plugin
3    * (c) Copyright 2016 Gerrit Hohl
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package net.sourceforge.javadpkg.impl;
20  
21  import java.io.BufferedWriter;
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.OutputStreamWriter;
29  import java.util.HashMap;
30  import java.util.Map;
31  
32  import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
33  import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
34  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
35  import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
36  import org.apache.commons.compress.compressors.CompressorOutputStream;
37  import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
38  import org.apache.commons.compress.compressors.gzip.GzipParameters;
39  
40  import net.sourceforge.javadpkg.BuildException;
41  import net.sourceforge.javadpkg.ChangeLog;
42  import net.sourceforge.javadpkg.ChangeLogBuilder;
43  import net.sourceforge.javadpkg.Context;
44  import net.sourceforge.javadpkg.Copyright;
45  import net.sourceforge.javadpkg.CopyrightBuilder;
46  import net.sourceforge.javadpkg.DebianPackageBuilder;
47  import net.sourceforge.javadpkg.DebianPackageConstants;
48  import net.sourceforge.javadpkg.DocumentPaths;
49  import net.sourceforge.javadpkg.MD5SumsBuilder;
50  import net.sourceforge.javadpkg.Script;
51  import net.sourceforge.javadpkg.ScriptBuilder;
52  import net.sourceforge.javadpkg.ScriptVariableReplacer;
53  import net.sourceforge.javadpkg.control.BinaryControl;
54  import net.sourceforge.javadpkg.control.Control;
55  import net.sourceforge.javadpkg.control.ControlBuilder;
56  import net.sourceforge.javadpkg.control.PackageVersion;
57  import net.sourceforge.javadpkg.control.Size;
58  import net.sourceforge.javadpkg.control.impl.ControlBuilderImpl;
59  import net.sourceforge.javadpkg.io.DataSource;
60  import net.sourceforge.javadpkg.io.DataSwap;
61  import net.sourceforge.javadpkg.io.DataTarget;
62  import net.sourceforge.javadpkg.io.FileMode;
63  import net.sourceforge.javadpkg.io.FileOwner;
64  import net.sourceforge.javadpkg.io.Streams;
65  import net.sourceforge.javadpkg.io.impl.DataStreamTarget;
66  import net.sourceforge.javadpkg.io.impl.DataTempFileSwap;
67  import net.sourceforge.javadpkg.replace.ReplacementException;
68  import net.sourceforge.javadpkg.replace.Replacements;
69  import net.sourceforge.javadpkg.replace.ReplacementsMap;
70  import net.sourceforge.javadpkg.store.DataStore;
71  import net.sourceforge.javadpkg.store.DataStoreImpl;
72  
73  
74  /**
75   * <p>
76   * A {@link DebianPackageBuilder} implementation.
77   * </p>
78   *
79   * @author Gerrit Hohl (gerrit-hohl@users.sourceforge.net)
80   * @version <b>1.0</b>, 25.04.2016 by Gerrit Hohl
81   */
82  public class DebianPackageBuilderImpl implements DebianPackageBuilder, DebianPackageConstants {
83  	
84  	
85  	/**
86  	 * <p>
87  	 * The Debian package file format version written by this builder
88  	 * implementation.
89  	 * </p>
90  	 */
91  	private static final String		FILE_FORMAT_VERSION	= "2.0";
92  	
93  	
94  	/**
95  	 * <p>
96  	 * The default script which will be executed before the package is
97  	 * installed.
98  	 * </p>
99  	 */
100 	private Script					defaultPreInstall;
101 	/**
102 	 * <p>
103 	 * The default script which will be executed after the package is installed.
104 	 * </p>
105 	 */
106 	private Script					defaultPostInstall;
107 	/**
108 	 * <p>
109 	 * The default script which will be executed before the package is removed.
110 	 * </p>
111 	 */
112 	private Script					defaultPreRemove;
113 	/**
114 	 * <p>
115 	 * The default script which will be executed after the package is removed.
116 	 * </p>
117 	 */
118 	private Script					defaultPostRemove;
119 	
120 	/** The builder for the control. */
121 	private ControlBuilder			controlBuilder;
122 	/** The builder for the MD5 sums. */
123 	private MD5SumsBuilder			md5SumsBuilder;
124 	/** The builder for the scripts. */
125 	private ScriptBuilder			scriptBuilder;
126 	/** The variable replacer for the scripts. */
127 	private ScriptVariableReplacer	scriptVariableReplacer;
128 	/** The builder for the copyright. */
129 	private CopyrightBuilder		copyrightBuilder;
130 	/** The builder for the change log. */
131 	private ChangeLogBuilder		changeLogBuilder;
132 	
133 	/** The control information. */
134 	private Control					control;
135 	/** The overhead which will be added to the specified installed size. */
136 	private Size					installedSizeOverhead;
137 	/** The script which will be executed before the package is installed. */
138 	private Script					preInstall;
139 	/** The script which will be executed after the package is installed. */
140 	private Script					postInstall;
141 	/** The script which will be executed before the package is removed. */
142 	private Script					preRemove;
143 	/** The script which will be executed after the package is removed. */
144 	private Script					postRemove;
145 	/** The store for the data files. */
146 	private DataStore				dataStore;
147 	/** The copyright. */
148 	private Copyright				copyright;
149 	/** The change log. */
150 	private ChangeLog				changeLog;
151 	
152 	
153 	/**
154 	 * <p>
155 	 * Creates a builder.
156 	 * </p>
157 	 *
158 	 * @param defaultPreInstall
159 	 *            The default script which will be executed before the package
160 	 *            is installed.
161 	 * @param defaultPostInstall
162 	 *            The default script which will be executed after the package is
163 	 *            installed.
164 	 * @param defaultPreRemove
165 	 *            The default script which will be executed before the package
166 	 *            is removed.
167 	 * @param defaultPostRemove
168 	 *            The default script which will be executed after the package is
169 	 *            removed.
170 	 * @throws IllegalArgumentException
171 	 *             If any of the parameters are <code>null</code>.
172 	 */
173 	protected DebianPackageBuilderImpl(Script defaultPreInstall, Script defaultPostInstall, Script defaultPreRemove,
174 			Script defaultPostRemove) {
175 		
176 		super();
177 		
178 		if (defaultPreInstall == null)
179 			throw new IllegalArgumentException("Argument defaultPreInstall is null.");
180 		if (defaultPostInstall == null)
181 			throw new IllegalArgumentException("Argument defaultPostInstall is null.");
182 		if (defaultPreRemove == null)
183 			throw new IllegalArgumentException("Argument defaultPreRemove is null.");
184 		if (defaultPostRemove == null)
185 			throw new IllegalArgumentException("Argument defaultPostRemove is null.");
186 		
187 		this.defaultPreInstall = defaultPreInstall;
188 		this.defaultPostInstall = defaultPostInstall;
189 		this.defaultPreRemove = defaultPreRemove;
190 		this.defaultPostRemove = defaultPostRemove;
191 		
192 		this.controlBuilder = new ControlBuilderImpl();
193 		this.md5SumsBuilder = new MD5SumsBuilderImpl();
194 		this.scriptBuilder = new ScriptBuilderImpl();
195 		this.scriptVariableReplacer = new ScriptVariableReplacerImpl();
196 		this.copyrightBuilder = new CopyrightBuilderImpl();
197 		this.changeLogBuilder = new ChangeLogBuilderImpl();
198 		
199 		this.control = null;
200 		this.installedSizeOverhead = null;
201 		this.preInstall = null;
202 		this.postInstall = null;
203 		this.preRemove = null;
204 		this.postRemove = null;
205 		this.dataStore = new DataStoreImpl();
206 		this.copyright = null;
207 		this.changeLog = null;
208 	}
209 	
210 	
211 	@Override
212 	public void setControl(Control control) {
213 		this.control = control;
214 	}
215 	
216 	
217 	@Override
218 	public void setInstalledSizeOverhead(Size installedSizeOverhead) {
219 		this.installedSizeOverhead = installedSizeOverhead;
220 	}
221 	
222 	
223 	@Override
224 	public void setPreInstall(Script preInstall) {
225 		this.preInstall = preInstall;
226 	}
227 	
228 	
229 	@Override
230 	public void setPostInstall(Script postInstall) {
231 		this.postInstall = postInstall;
232 	}
233 	
234 	
235 	@Override
236 	public void setPreRemove(Script preRemove) {
237 		this.preRemove = preRemove;
238 	}
239 	
240 	
241 	@Override
242 	public void setPostRemove(Script postRemove) {
243 		this.postRemove = postRemove;
244 	}
245 	
246 	
247 	@Override
248 	public void addDataDirectory(String path) {
249 		if (path == null)
250 			throw new IllegalArgumentException("Argument path is null.");
251 		
252 		this.dataStore.addDirectory(path);
253 	}
254 
255 
256 	@Override
257 	public void addDataDirectory(String path, FileOwner owner, FileMode mode) {
258 		if (path == null)
259 			throw new IllegalArgumentException("Argument path is null.");
260 		if (owner == null)
261 			throw new IllegalArgumentException("Argument owner is null.");
262 		if (mode == null)
263 			throw new IllegalArgumentException("Argument mode is null.");
264 		
265 		this.dataStore.addDirectory(path, owner, mode);
266 	}
267 
268 
269 	@Override
270 	public void addDataFile(DataSource source, String path) {
271 		if (source == null)
272 			throw new IllegalArgumentException("Argument source is null.");
273 		if (path == null)
274 			throw new IllegalArgumentException("Argument path is null.");
275 		
276 		this.dataStore.addFile(source, path);
277 	}
278 
279 
280 	@Override
281 	public void addDataFile(DataSource source, String path, FileOwner owner, FileMode mode) {
282 		
283 		if (source == null)
284 			throw new IllegalArgumentException("Argument source is null.");
285 		if (path == null)
286 			throw new IllegalArgumentException("Argument path is null.");
287 		if (owner == null)
288 			throw new IllegalArgumentException("Argument owner is null.");
289 		if (mode == null)
290 			throw new IllegalArgumentException("Argument mode is null.");
291 		
292 		this.dataStore.addFile(source, path, owner, mode);
293 	}
294 	
295 	
296 	@Override
297 	public void addDataSymLink(String path, String target, FileOwner owner, FileMode mode) {
298 		
299 		if (path == null)
300 			throw new IllegalArgumentException("Argument path is null.");
301 		if (target == null)
302 			throw new IllegalArgumentException("Argument target is null.");
303 		if (owner == null)
304 			throw new IllegalArgumentException("Argument owner is null.");
305 		if (mode == null)
306 			throw new IllegalArgumentException("Argument mode is null.");
307 		
308 		this.dataStore.addSymLink(path, target, owner, mode);
309 	}
310 	
311 	
312 	@Override
313 	public void setCopyright(Copyright copyright) {
314 		this.copyright = copyright;
315 	}
316 
317 
318 	@Override
319 	public void setChangeLog(ChangeLog changeLog) {
320 		this.changeLog = changeLog;
321 	}
322 	
323 	
324 	@Override
325 	public void buildDebianPackage(DataTarget target, Context context) throws IOException, BuildException {
326 		
327 		
328 		if (target == null)
329 			throw new IllegalArgumentException("Argument target is null.");
330 		if (context == null)
331 			throw new IllegalArgumentException("Argument context is null.");
332 		
333 		if (this.control == null)
334 			throw new IllegalStateException("Can't build Debian package because no control information is set.");
335 		if (this.copyright == null)
336 			throw new IllegalStateException("Can't build Debian package because no copyright is set.");
337 		if (this.changeLog == null)
338 			throw new IllegalStateException("Can't build Debian package because no change log is set.");
339 		
340 		// TODO Perform a consistency check of the configuration of the Debian package.
341 		try (OutputStream out = target.getOutputStream()) {
342 			try (ArArchiveOutputStream arOut = new ArArchiveOutputStream(out)) {
343 				// --- Write the version of the package ---
344 				this.writeVersion(target, arOut);
345 
346 				// --- Write the meta information ---
347 				this.writeControl(target, context, arOut);
348 
349 				// --- Write the contents ---
350 				this.writeData(target, context, arOut);
351 			}
352 		}
353 	}
354 
355 
356 	/**
357 	 * <p>
358 	 * Writes the version of the Debian package.
359 	 * </p>
360 	 *
361 	 * @param target
362 	 *            The target.
363 	 * @param out
364 	 *            The stream on the archive.
365 	 * @throws IOException
366 	 *             If an I/O error occurs.
367 	 */
368 	private void writeVersion(DataTarget target, ArArchiveOutputStream out) throws IOException {
369 		ByteArrayOutputStream arrayOut;
370 		byte[] data;
371 		ArArchiveEntry entry;
372 
373 
374 		try {
375 			// --- Prepare the data (we need the exact length) ---
376 			arrayOut = new ByteArrayOutputStream();
377 			try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(arrayOut, UTF_8_CHARSET))) {
378 				writer.write(FILE_FORMAT_VERSION);
379 				writer.write('\n');
380 			}
381 			data = arrayOut.toByteArray();
382 			
383 			// --- Write the entry (default flags are okay: UID: 0; GID: 0; MODE: 33188) ---
384 			entry = new ArArchiveEntry("debian-binary", data.length);
385 			out.putArchiveEntry(entry);
386 			out.write(data);
387 			out.closeArchiveEntry();
388 		} catch (IOException e) {
389 			throw new IOException("Couldn't write |debian-binary| in AR archive |" + target.getName() + "|: " + e.getMessage(),
390 					e);
391 		}
392 	}
393 
394 
395 	/**
396 	 * <p>
397 	 * Writes the meta information of the Debian package.
398 	 * </p>
399 	 *
400 	 * @param target
401 	 *            The target.
402 	 * @param context
403 	 *            The context.
404 	 * @param out
405 	 *            The stream on the archive.
406 	 * @throws IOException
407 	 *             If an I/O error occurs.
408 	 * @throws BuildException
409 	 *             If an error occurs during the building.
410 	 */
411 	private void writeControl(DataTarget target, Context context, ArArchiveOutputStream out)
412 			throws IOException, BuildException {
413 		
414 		File controlFile;
415 		Size installedSize = null;
416 		PackageVersion version;
417 		boolean deleted = false;
418 		
419 		
420 		// --- Get the installed size of the package (from the control or by counting) ---
421 		if (this.control instanceof BinaryControl) {
422 			installedSize = ((BinaryControl) this.control).getInstalledSize();
423 		}
424 		if (installedSize == null) {
425 			try {
426 				installedSize = this.dataStore.getSize();
427 			} catch (IOException e) {
428 				throw new IOException(
429 						"Couldn't determine size of all files which should be added to the Debian package: " + e.getMessage(),
430 						e);
431 			}
432 		}
433 		// --- Add the overhead (if available) ---
434 		if (this.installedSizeOverhead != null) {
435 			installedSize = new Size(installedSize.getBytes() + this.installedSizeOverhead.getBytes());
436 		}
437 
438 		/*
439 		 * --- Write the control file.
440 		 *     Unfortunately we can't write it directly into the AR archive because of the
441 		 *     commons-compress implementation which needs the size before storing the entry. ---
442 		 */
443 		controlFile = File.createTempFile(CONTROL_NAME, TAR_GZIP_SUFFIX);
444 		try {
445 			try (FileOutputStream fileOut = new FileOutputStream(controlFile)) {
446 				try (TarArchiveOutputStream tarOut = this.createTarArchive(fileOut)) {
447 					// --- Root entry ---
448 					this.writeTarEntry(tarOut, "", ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME, DIRECTORY_MODE,
449 							null);
450 					
451 					// --- Control information ---
452 					try (DataSwap swap = new DataTempFileSwap(CONTROL_ENTRY)) {
453 						this.controlBuilder.buildControl(this.control, installedSize, swap.getTarget(), context);
454 						this.writeTarEntry(tarOut, CONTROL_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
455 								FILE_MODE, swap.getSource());
456 					}
457 					
458 					// --- MD5 sums ---
459 					try (DataSwap swap = new DataTempFileSwap(MD5SUMS_ENTRY)) {
460 						this.md5SumsBuilder.buildMD5Sums(this.dataStore, swap.getTarget());
461 						this.writeTarEntry(tarOut, MD5SUMS_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
462 								FILE_MODE, swap.getSource());
463 					}
464 					
465 					
466 					// --- Scripts are only used for binary packages ---
467 					if (this.control instanceof BinaryControl) {
468 						version = ((BinaryControl) this.control).getVersion();
469 						
470 						// --- Pre-installation script ---
471 						this.writeScript(tarOut, this.preInstall, this.defaultPreInstall, PREINST_ENTRY, version, context);
472 						
473 						// --- Post-installation script ---
474 						this.writeScript(tarOut, this.postInstall, this.defaultPostInstall, POSTINST_ENTRY, version, context);
475 						
476 						// --- Pre-removal script ---
477 						this.writeScript(tarOut, this.preRemove, this.defaultPreRemove, PRERM_ENTRY, version, context);
478 						
479 						// --- Post-removal script ---
480 						this.writeScript(tarOut, this.postRemove, this.defaultPostRemove, POSTRM_ENTRY, version, context);
481 					}
482 					
483 					// --- Templates ---
484 					// TODO Write templates.
485 					//					this.createTarEntry(tarOut, TEMPLATES_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
486 					//							FILE_MODE);
487 					//					tarOut.closeArchiveEntry();
488 					
489 					// --- Configuration ---
490 					// TODO Write configuration.
491 					//					this.createTarEntry(tarOut, CONFIG_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
492 					//							SCRIPT_MODE);
493 					//					tarOut.closeArchiveEntry();
494 					
495 					// --- Configuration files ---
496 					// TODO Write configuration files.
497 					//					this.createTarEntry(tarOut, CONFFILES_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
498 					//							FILE_MODE);
499 					//					tarOut.closeArchiveEntry();
500 					
501 					// --- Shared libraries ---
502 					// TODO Write shared libraries.
503 					//					this.createTarEntry(tarOut, SHLIBS_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
504 					//							FILE_MODE);
505 					//					tarOut.closeArchiveEntry();
506 					
507 					// --- Symbols ---
508 					// TODO Write symbols.
509 					//					this.createTarEntry(tarOut, SYMBOLS_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
510 					//							FILE_MODE);
511 					//					tarOut.closeArchiveEntry();
512 					
513 					// --- Triggers ---
514 					// TODO Write triggers.
515 					//					this.createTarEntry(tarOut, TRIGGERS_ENTRY, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME,
516 					//							FILE_MODE);
517 					//					tarOut.closeArchiveEntry();
518 				}
519 			}
520 			
521 			// --- Write the entry (default flags are okay: UID: 0; GID: 0; MODE: 33188) ---
522 			this.writeArEntry(out, CONTROL_NAME + TAR_GZIP_SUFFIX, controlFile);
523 		} finally {
524 			// --- Try to delete the control file ---
525 			if (controlFile.exists() && !controlFile.delete()) {
526 				deleted = false;
527 			} else {
528 				deleted = true;
529 			}
530 		}
531 		if (!deleted)
532 			throw new IOException("Couldn't delete temporary control file |" + controlFile.getAbsolutePath() + "|.");
533 	}
534 
535 
536 	/**
537 	 * <p>
538 	 * Writes an installation or removal script.
539 	 * </p>
540 	 *
541 	 * @param out
542 	 *            The stream.
543 	 * @param script
544 	 *            The script.
545 	 * @param defaultScript
546 	 *            The default script if the no script is passed.
547 	 * @param name
548 	 *            The name of the entry.
549 	 * @param version
550 	 *            The version.
551 	 * @param context
552 	 *            The context.
553 	 * @throws IOException
554 	 *             If an I/O error occurs.
555 	 * @throws BuildException
556 	 *             If an error occurs during the build.
557 	 */
558 	private void writeScript(TarArchiveOutputStream out, Script script, Script defaultScript, String name,
559 			PackageVersion version, Context context) throws IOException, BuildException {
560 		
561 		Script effectiveScript;
562 		Map<String, String> variables;
563 		Replacements replacements;
564 		
565 		
566 		if (script == null) {
567 			variables = new HashMap<>();
568 			// TODO Extract to constants. Maybe we should use Replacements from outside which includes the control properties.
569 			variables.put("deb.version", version.getText());
570 			replacements = new ReplacementsMap(variables);
571 			try {
572 				effectiveScript = this.scriptVariableReplacer.replaceScriptVariables(defaultScript, replacements, context);
573 			} catch (ReplacementException e) {
574 				throw new BuildException("Couldn't write default script |" + name + "|: " + e.getMessage(), e);
575 			}
576 		} else {
577 			effectiveScript = script;
578 		}
579 
580 		try (DataSwap swap = new DataTempFileSwap(name)) {
581 			this.scriptBuilder.buildScript(swap.getTarget(), effectiveScript);
582 			this.writeTarEntry(out, name, ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME, SCRIPT_MODE,
583 					swap.getSource());
584 		}
585 	}
586 
587 
588 	/**
589 	 * <p>
590 	 * Writes the contents of the Debian package.
591 	 * </p>
592 	 *
593 	 * @param target
594 	 *            The target.
595 	 * @param context
596 	 *            The context.
597 	 * @param out
598 	 *            The stream on the archive.
599 	 * @throws IOException
600 	 *             If an I/O error occurs.
601 	 * @throws BuildException
602 	 *             If an error occurs during the build.
603 	 */
604 	@SuppressWarnings("resource")
605 	private void writeData(DataTarget target, Context context, ArArchiveOutputStream out) throws IOException, BuildException {
606 		BinaryControl control;
607 		DocumentPaths paths;
608 		String[] ensurePaths;
609 		File dataFile;
610 		boolean deleted = false;
611 		DataSwap swapCopyright = null, swapChangeLog = null;
612 		
613 		
614 		// --- Create the document paths ---
615 		if (this.control instanceof BinaryControl) {
616 			control = (BinaryControl) this.control;
617 		} else
618 			throw new BuildException("Found control |" + this.control + "| of type |"
619 					+ (this.control == null ? "null" : this.control.getClass().getCanonicalName())
620 					+ ", but only control of type |" + BinaryControl.class.getCanonicalName() + "| is supported.");
621 		paths = new DocumentPathsImpl(control.getPackage());
622 		
623 		
624 		// --- Make sure the copyright and the change log are not added directly ---
625 		// TODO Check for changelog.Debian, changelog.Debian.gz and so on.
626 		if (this.dataStore.exists(paths.getCopyrightPath()))
627 			throw new BuildException("Found |" + paths.getCopyrightPath()
628 					+ " added to the builder, but the copyright should be added through the corresponding method of the builder.");
629 		else if (this.dataStore.exists(paths.getChangeLogPath()))
630 			throw new BuildException("Found |" + paths.getChangeLogPath()
631 					+ " added to the builder, but the copyright should be added through the corresponding method of the builder.");
632 		else if (this.dataStore.exists(paths.getChangeLogGzipPath()))
633 			throw new BuildException("Found |" + paths.getChangeLogGzipPath()
634 					+ " added to the builder, but the copyright should be added through the corresponding method of the builder.");
635 		else if (this.dataStore.exists(paths.getChangeLogHtmlPath()))
636 			throw new BuildException("Found |" + paths.getChangeLogHtmlPath()
637 					+ " added to the builder, but the copyright should be added through the corresponding method of the builder.");
638 		else if (this.dataStore.exists(paths.getChangeLogHtmlGzipPath()))
639 			throw new BuildException("Found |" + paths.getChangeLogHtmlGzipPath()
640 					+ " added to the builder, but the copyright should be added through the corresponding method of the builder.");
641 		
642 		// --- Make sure the document folder for the Debian package exists ---
643 		ensurePaths = new String[] { USR_PATH, USR_SHARE_PATH, paths.getDocumentBasePath(), paths.getDocumentPath() };
644 		for (String ensurePath : ensurePaths) {
645 			if (!this.dataStore.exists(ensurePath)) {
646 				this.dataStore.addDirectory(ensurePath);
647 			}
648 		}
649 		
650 		/*
651 		 * --- Write the data file.
652 		 *     Unfortunately we can't write it directly into the AR archive because of the
653 		 *     commons-compress implementation which needs the size before storing the entry. ---
654 		 */
655 		dataFile = File.createTempFile(DATA_NAME, TAR_GZIP_SUFFIX);
656 		try {
657 			// --- Create the copyright ---
658 			swapCopyright = new DataTempFileSwap("copyright");
659 			this.copyrightBuilder.buildCopyright(this.copyright, swapCopyright.getTarget(), context);
660 			this.dataStore.addFile(swapCopyright.getSource(), paths.getCopyrightPath());
661 			
662 			try (DataSwap swap = new DataTempFileSwap("changelog")) {
663 				this.changeLogBuilder.buildChangeLog(this.changeLog, swap.getTarget(), context);
664 				swapChangeLog = new DataTempFileSwap("changelog.gz");
665 				Streams.compressGzip(swap.getSource(), swapChangeLog.getTarget(), 9);
666 			}
667 			this.dataStore.addFile(swapChangeLog.getSource(), paths.getChangeLogGzipPath());
668 			
669 			// --- Write all directories and files of the data store ---
670 			try (FileOutputStream fileOut = new FileOutputStream(dataFile)) {
671 				try (TarArchiveOutputStream tarOut = this.createTarArchive(fileOut)) {
672 					this.dataStore.write(tarOut);
673 				}
674 			}
675 			
676 			// --- Write the entry (default flags are okay: UID: 0; GID: 0; MODE: 33188) ---
677 			this.writeArEntry(out, DATA_NAME + TAR_GZIP_SUFFIX, dataFile);
678 		} finally {
679 			// --- Close change log ---
680 			swapChangeLog.close();
681 			
682 			// --- Close copyright ---
683 			swapCopyright.close();
684 			
685 			// --- Try to delete the control file ---
686 			if (dataFile.exists() && !dataFile.delete()) {
687 				deleted = false;
688 			} else {
689 				deleted = true;
690 			}
691 		}
692 		if (!deleted)
693 			throw new IOException("Couldn't delete temporary data file |" + dataFile.getAbsolutePath() + "|.");
694 	}
695 	
696 	
697 	/**
698 	 * <p>
699 	 * Creates TAR archive upon the specified stream.
700 	 * </p>
701 	 *
702 	 * @param out
703 	 *            The underlying stream.
704 	 * @return The stream on the TAR archive.
705 	 * @throws IOException
706 	 *             If an I/O error occurs while opening the TAR archive.
707 	 */
708 	@SuppressWarnings("resource")
709 	private TarArchiveOutputStream createTarArchive(OutputStream out) throws IOException {
710 		TarArchiveOutputStream tarOut;
711 		GzipParameters gzipParameters;
712 		CompressorOutputStream compressorOut;
713 		
714 		
715 		// ROADMAP Configurable compression (GZIP / BZIP2 / XZ).
716 		gzipParameters = new GzipParameters();
717 		gzipParameters.setCompressionLevel(9);
718 		compressorOut = new GzipCompressorOutputStream(out, gzipParameters);
719 		
720 		tarOut = new TarArchiveOutputStream(compressorOut);
721 		tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
722 		return tarOut;
723 	}
724 	
725 	
726 	/**
727 	 * <p>
728 	 * Writes a entry into the TAR archive.
729 	 * </p>
730 	 *
731 	 * @param out
732 	 *            The stream on the TAR archive.
733 	 * @param name
734 	 *            The name of the entry.
735 	 * @param groupId
736 	 *            The group ID.
737 	 * @param groupName
738 	 *            The group name.
739 	 * @param userId
740 	 *            The user ID.
741 	 * @param userName
742 	 *            The user name.
743 	 * @param mode
744 	 *            The mode.
745 	 * @param source
746 	 *            The source for the content of the entry (optional).
747 	 * @throws IOException
748 	 *             If an I/O error occurs.
749 	 */
750 	private void writeTarEntry(TarArchiveOutputStream out, String name, long groupId, String groupName, long userId,
751 			String userName, int mode, DataSource source) throws IOException {
752 		
753 		StringBuilder entryName;
754 		TarArchiveEntry tarEntry;
755 		
756 		
757 		// --- Create entry name ---
758 		entryName = new StringBuilder();
759 		if (!name.startsWith("./")) {
760 			entryName.append('.');
761 			if (!name.startsWith("/")) {
762 				entryName.append('/');
763 			}
764 		}
765 		entryName.append(name);
766 		
767 		// --- Open entry ---
768 		tarEntry = new TarArchiveEntry(entryName.toString());
769 		tarEntry.setGroupId(groupId);
770 		tarEntry.setGroupName(groupName);
771 		tarEntry.setUserId(userId);
772 		tarEntry.setUserName(userName);
773 		tarEntry.setMode(mode);
774 		if (source != null) {
775 			if (source.getLength() < 0)
776 				throw new IOException("Couldn't create entry |" + name
777 						+ "| in TAR archive stream: Couldn't determine size of source |" + source.getName() + "|.");
778 			tarEntry.setSize(source.getLength());
779 		}
780 		try {
781 			out.putArchiveEntry(tarEntry);
782 		} catch (IOException e) {
783 			throw new IOException("Couldn't create entry |" + name + "| in TAR archive stream: " + e.getMessage(), e);
784 		}
785 
786 		// --- Copy the content ---
787 		if (source != null) {
788 			try {
789 				if (source.getLength() > 0) {
790 					try (DataTarget target = new DataStreamTarget(out, name, false)) {
791 						Streams.copy(source, target);
792 					}
793 				}
794 			} catch (IOException e) {
795 				throw new IOException("Couldn't create entry |" + name + "| in TAR archive stream: " + e.getMessage(), e);
796 			}
797 		}
798 
799 		// --- Close entry ---
800 		try {
801 			out.closeArchiveEntry();
802 		} catch (IOException e) {
803 			throw new IOException("Couldn't create entry |" + name + "| in TAR archive stream: " + e.getMessage(), e);
804 		}
805 	}
806 
807 
808 	/**
809 	 * <p>
810 	 * Writes a entry into the AR archive.
811 	 * </p>
812 	 *
813 	 * @param out
814 	 *            The stream on the AR archive.
815 	 * @param name
816 	 *            The name of the entry.
817 	 * @param file
818 	 *            The file having the content of the entry.
819 	 * @throws IOException
820 	 *             If an I/O error occurs.
821 	 */
822 	private void writeArEntry(ArArchiveOutputStream out, String name, File file) throws IOException {
823 		ArArchiveEntry entry;
824 
825 
826 		try {
827 			entry = new ArArchiveEntry(name, file.length());
828 			out.putArchiveEntry(entry);
829 			try (InputStream in = Streams.createBufferedFileInputStream(file)) {
830 				Streams.copy(in, out);
831 			}
832 			out.closeArchiveEntry();
833 		} catch (IOException e) {
834 			throw new IOException("Couldn't write file |" + file.getAbsolutePath() + "| as entry |" + name
835 					+ "| into the archive stream: " + e.getMessage(), e);
836 		}
837 	}
838 
839 
840 }