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.store;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.security.MessageDigest;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  
31  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
32  import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
33  import org.apache.commons.compress.archivers.tar.TarConstants;
34  
35  import net.sourceforge.javadpkg.DebianPackageConstants;
36  import net.sourceforge.javadpkg.io.DataConsumer;
37  import net.sourceforge.javadpkg.io.DataProducer;
38  import net.sourceforge.javadpkg.io.DataSource;
39  import net.sourceforge.javadpkg.io.FileMode;
40  import net.sourceforge.javadpkg.io.FileOwner;
41  import net.sourceforge.javadpkg.io.Streams;
42  import net.sourceforge.javadpkg.io.impl.DataDigestConsumer;
43  import net.sourceforge.javadpkg.io.impl.FileModeImpl;
44  import net.sourceforge.javadpkg.io.impl.FileOwnerImpl;
45  
46  /**
47   * <p>
48   * A node representing a directory or file in the {@link DataStore}.
49   * </p>
50   *
51   * @author Gerrit Hohl (gerrit-hohl@users.sourceforge.net)
52   * @version <b>1.0</b>, 26.04.2016 by Gerrit Hohl
53   */
54  public class DataStoreNode implements DebianPackageConstants {
55  	
56  	
57  	/** The source of the file content. */
58  	private DataSource					source;
59  	/** The name. */
60  	private String						name;
61  	/** The target of the symbolic link. */
62  	private String						target;
63  	/** The owner. */
64  	private FileOwner					owner;
65  	/** The mode. */
66  	private FileMode					mode;
67  	
68  	/** The child nodes. */
69  	private Map<String, DataStoreNode>	childNodes;
70  	/** The parent node. */
71  	private DataStoreNode				parentNode;
72  	
73  	
74  	/**
75  	 * <p>
76  	 * Creates the root node.
77  	 * </p>
78  	 */
79  	public DataStoreNode() {
80  		super();
81  		
82  		this.source = null;
83  		this.name = "";
84  		this.target = null;
85  		this.owner = new FileOwnerImpl(ROOT_GROUP_ID, ROOT_GROUP_NAME, ROOT_USER_ID, ROOT_USER_NAME);
86  		this.mode = new FileModeImpl(DIRECTORY_MODE);
87  		this.childNodes = new HashMap<>();
88  		this.parentNode = null;
89  	}
90  
91  
92  	/**
93  	 * <p>
94  	 * Creates a node for a directory.
95  	 * </p>
96  	 *
97  	 * @param name
98  	 *            The name.
99  	 * @param owner
100 	 *            The owner.
101 	 * @param mode
102 	 *            The mode.
103 	 * @throws IllegalArgumentException
104 	 *             If any of the parameters are <code>null</code>.
105 	 */
106 	public DataStoreNode(String name, FileOwner owner, FileMode mode) {
107 		super();
108 
109 		if (name == null)
110 			throw new IllegalArgumentException("Argument name is null.");
111 		if (owner == null)
112 			throw new IllegalArgumentException("Argument owner is null.");
113 		if (mode == null)
114 			throw new IllegalArgumentException("Argument mode is null.");
115 		
116 		this.source = null;
117 		this.name = name;
118 		this.target = null;
119 		this.owner = owner;
120 		this.mode = mode;
121 		this.childNodes = new HashMap<>();
122 		this.parentNode = null;
123 	}
124 
125 
126 	/**
127 	 * <p>
128 	 * Creates a node for a file.
129 	 * </p>
130 	 *
131 	 * @param source
132 	 *            The source for the content of the file.
133 	 * @param name
134 	 *            The name.
135 	 * @param owner
136 	 *            The owner.
137 	 * @param mode
138 	 *            The mode.
139 	 * @throws IllegalArgumentException
140 	 *             If any of the parameters are <code>null</code>.
141 	 */
142 	public DataStoreNode(DataSource source, String name, FileOwner owner, FileMode mode) {
143 		
144 		super();
145 		
146 		if (source == null)
147 			throw new IllegalArgumentException("Argument source is null.");
148 		if (name == null)
149 			throw new IllegalArgumentException("Argument name is null.");
150 		if (owner == null)
151 			throw new IllegalArgumentException("Argument owner is null.");
152 		if (mode == null)
153 			throw new IllegalArgumentException("Argument mode is null.");
154 		
155 		this.source = source;
156 		this.name = name;
157 		this.target = null;
158 		this.owner = owner;
159 		this.mode = mode;
160 		this.childNodes = null;
161 		this.parentNode = null;
162 	}
163 	
164 	
165 	/**
166 	 * <p>
167 	 * Creates a node for a symbolic link.
168 	 * </p>
169 	 *
170 	 * @param name
171 	 *            The name.
172 	 * @param target
173 	 *            The target of the symbolic link.
174 	 * @param owner
175 	 *            The owner.
176 	 * @param mode
177 	 *            The mode.
178 	 * @throws IllegalArgumentException
179 	 *             If any of the parameters are <code>null</code>.
180 	 */
181 	public DataStoreNode(String name, String target, FileOwner owner, FileMode mode) {
182 		super();
183 		
184 		if (name == null)
185 			throw new IllegalArgumentException("Argument name is null.");
186 		if (target == null)
187 			throw new IllegalArgumentException("Argument target is null.");
188 		if (owner == null)
189 			throw new IllegalArgumentException("Argument owner is null.");
190 		if (mode == null)
191 			throw new IllegalArgumentException("Argument mode is null.");
192 		
193 		this.source = null;
194 		this.name = name;
195 		this.target = target;
196 		this.owner = owner;
197 		this.mode = mode;
198 		this.childNodes = null;
199 		this.parentNode = null;
200 	}
201 	
202 	
203 	/**
204 	 * <p>
205 	 * Returns the name.
206 	 * </p>
207 	 *
208 	 * @return The name.
209 	 */
210 	public String getName() {
211 		return this.name;
212 	}
213 
214 
215 	/**
216 	 * <p>
217 	 * Returns the path.
218 	 * </p>
219 	 *
220 	 * @return The path.
221 	 */
222 	public String getPath() {
223 		StringBuilder sb;
224 		
225 		
226 		sb = new StringBuilder();
227 		this.addPath(sb);
228 		return sb.toString();
229 	}
230 
231 
232 	/**
233 	 * <p>
234 	 * Adds the path to the specified builder.
235 	 * </p>
236 	 *
237 	 * @param builder
238 	 *            The builder.
239 	 */
240 	private void addPath(StringBuilder builder) {
241 		if (this.parentNode != null) {
242 			this.parentNode.addPath(builder);
243 		}
244 		builder.append(this.name);
245 		if (this.childNodes != null) {
246 			builder.append('/');
247 		}
248 	}
249 
250 
251 	/**
252 	 * <p>
253 	 * Returns the flag if the node is a directory node.
254 	 * </p>
255 	 *
256 	 * @return The flag: <code>true</code>, if the node is a directory node,
257 	 *         <code>false</code> otherwise.
258 	 * @see #isFile()
259 	 */
260 	public boolean isDirectory() {
261 		return (this.childNodes != null);
262 	}
263 	
264 	
265 	/**
266 	 * <p>
267 	 * Returns the flag if the node is a file node.
268 	 * </p>
269 	 * <p>
270 	 * Symbolic links are also files.
271 	 * </p>
272 	 *
273 	 * @return The flag: <code>true</code>, if the node is a file node,
274 	 *         <code>false</code> otherwise.
275 	 * @see #isDirectory()
276 	 * @see #isSymbolicLink()
277 	 */
278 	public boolean isFile() {
279 		return ((this.source != null) || (this.target != null));
280 	}
281 	
282 	
283 	/**
284 	 * <p>
285 	 * Returns the flag if the node is a symbolic link node.
286 	 * </p>
287 	 *
288 	 * @return The flag: <code>true</code>, if the node is a symbolic link node,
289 	 *         <code>false</code> otherwise.
290 	 * @see #isFile()
291 	 */
292 	public boolean isSymbolicLink() {
293 		return (this.target != null);
294 	}
295 	
296 	
297 	/**
298 	 * <p>
299 	 * Returns the size of the files under this node and its sub-nodes.
300 	 * </p>
301 	 *
302 	 * @return The size in bytes.
303 	 * @throws IOException
304 	 *             If the size of a file can't be determined.
305 	 */
306 	public long getSize() throws IOException {
307 		long size = 0;
308 		
309 		
310 		// --- Directory? ---
311 		if (this.childNodes != null) {
312 			if (!this.childNodes.isEmpty()) {
313 				for (DataStoreNode node : this.childNodes.values()) {
314 					size += node.getSize();
315 				}
316 			}
317 		}
318 		// --- Otherwise it is a file (if it is not a symbolic link) ---
319 		else if (this.target == null) {
320 			size = this.source.getLength();
321 			if (size < 0)
322 				throw new IOException(
323 						"Couldn't determine size for file |" + this.getPath() + "| (source |" + this.source.getName() + "|).");
324 		}
325 		return size;
326 	}
327 	
328 	
329 	/**
330 	 * <p>
331 	 * Adds a child node.
332 	 * </p>
333 	 *
334 	 * @param childNode
335 	 *            The child node.
336 	 * @throws IllegalArgumentException
337 	 *             If the child node is <code>null</code>, this node is not a
338 	 *             directory node, the child node is already added to a node or
339 	 *             this node already contains a child node with the same name.
340 	 */
341 	public void addChildNode(DataStoreNode childNode) {
342 		if (childNode == null)
343 			throw new IllegalArgumentException("Argument childNode is null.");
344 		if (!this.isDirectory())
345 			throw new IllegalStateException("Can't add child node |" + childNode.getName() + "| because this node |"
346 					+ this.getPath() + "| is not a directory node.");
347 		if (childNode.getParentNode() != null)
348 			throw new IllegalStateException("Can't add child node |" + childNode.getName()
349 					+ "| because the child node is already added to parent node |" + childNode.getParentNode() + "|.");
350 		if (this.childNodes.containsKey(childNode.getName()))
351 			throw new IllegalStateException("Can't add child node |" + childNode.getName() + "| because this node |"
352 					+ this.getPath() + "| already contains a child node with that name.");
353 		
354 		childNode.setParentNode(this);
355 		this.childNodes.put(childNode.getName(), childNode);
356 	}
357 
358 
359 	/**
360 	 * <p>
361 	 * Returns the child node with the specified name.
362 	 * </p>
363 	 *
364 	 * @param name
365 	 *            The name.
366 	 * @return The node or <code>null</code>, if this node doesn't contain such
367 	 *         a node.
368 	 * @throws IllegalArgumentException
369 	 *             If the name is <code>null</code> or this node is not a
370 	 *             directory node.
371 	 */
372 	public DataStoreNode getChildNodeByName(String name) {
373 		if (name == null)
374 			throw new IllegalArgumentException("Argument name is null.");
375 		if (this.childNodes == null)
376 			throw new IllegalArgumentException("Can't look for child node |" + name + "| because this node |" + this.getPath()
377 					+ "| is not a directory node.");
378 		
379 		return this.childNodes.get(name);
380 	}
381 	
382 	
383 	/**
384 	 * <p>
385 	 * Returns the child nodes as a list.
386 	 * </p>
387 	 *
388 	 * @param sorted
389 	 *            The flag if the child nodes should be sorted.
390 	 * @return The list.
391 	 */
392 	private List<DataStoreNode> getChildNodes(boolean sorted) {
393 		List<DataStoreNode> nodes;
394 
395 
396 		// --- Create a copy of the node list ---
397 		nodes = new ArrayList<>(this.childNodes.values());
398 
399 		// --- Sort nodes (if requested) ---
400 		if (sorted) {
401 			Collections.sort(nodes, new DataStoreNodeComparator());
402 		}
403 
404 		return nodes;
405 	}
406 	
407 	
408 	/**
409 	 * <p>
410 	 * Sets the parent node.
411 	 * </p>
412 	 *
413 	 * @param parentNode
414 	 *            The parent node.
415 	 */
416 	private void setParentNode(DataStoreNode parentNode) {
417 		this.parentNode = parentNode;
418 	}
419 
420 
421 	/**
422 	 * <p>
423 	 * Returns the parent node.
424 	 * </p>
425 	 *
426 	 * @return The parent node or <code>null</code>, if the node wasn't add to
427 	 *         any other node.
428 	 */
429 	public DataStoreNode getParentNode() {
430 		return this.parentNode;
431 	}
432 	
433 	
434 	/**
435 	 * <p>
436 	 * Writes the node and its child nodes into the stream.
437 	 * </p>
438 	 *
439 	 * @param out
440 	 *            The stream.
441 	 * @throws IOException
442 	 *             If an I/O error occurs.
443 	 * @throws IllegalArgumentException
444 	 *             If the stream is <code>null</code>.
445 	 */
446 	public void write(TarArchiveOutputStream out) throws IOException {
447 		String name;
448 		TarArchiveEntry entry;
449 		
450 		
451 		if (out == null)
452 			throw new IllegalArgumentException("Argument out is null.");
453 		
454 		// --- Create the TAR entry ---
455 		name = "." + this.getPath();
456 		// --- Do we have a directory or file? ---
457 		if (this.target == null) {
458 			entry = new TarArchiveEntry(name);
459 		}
460 		// --- Otherwise we have a symbolic link ---
461 		else {
462 			entry = new TarArchiveEntry(name, TarConstants.LF_SYMLINK);
463 		}
464 		entry.setGroupId(this.owner.getGroupId());
465 		entry.setGroupName(this.owner.getGroupName());
466 		entry.setUserId(this.owner.getUserId());
467 		entry.setUserName(this.owner.getUserName());
468 		entry.setMode(this.mode.getMode());
469 		// --- Set the length if we have a file ---
470 		if (this.source != null) {
471 			entry.setSize(this.source.getLength());
472 		}
473 		// --- Set the target if we have a symbolic link ---
474 		if (this.target != null) {
475 			entry.setLinkName(this.target);
476 		}
477 		out.putArchiveEntry(entry);
478 		
479 		// --- Write the contents if we have a file ---
480 		if (this.source != null) {
481 			this.writeSource(out);
482 		}
483 		out.closeArchiveEntry();
484 		
485 		// --- Write the child nodes if we have a directory ---
486 		if ((this.childNodes != null) && !this.childNodes.isEmpty()) {
487 			this.writeNodes(out);
488 		}
489 	}
490 	
491 	
492 	/**
493 	 * <p>
494 	 * Writes the content of the source into the stream.
495 	 * </p>
496 	 *
497 	 * @param out
498 	 *            The stream.
499 	 * @throws IOException
500 	 *             If an I/O error occurs.
501 	 */
502 	private void writeSource(OutputStream out) throws IOException {
503 		// --- Copy the content from the node source to the TAR entry ---
504 		try {
505 			try (InputStream in = this.source.getInputStream()) {
506 				Streams.copy(in, out);
507 			}
508 		} catch (IOException e) {
509 			throw new IOException("Couldn't write |" + this.getPath() + "| from source |" + this.source.getName()
510 					+ "| into the stream: " + e.getMessage());
511 		}
512 
513 		// --- Reset the source (if supported) ---
514 		if (this.source.isResettable()) {
515 			this.source.reset();
516 		}
517 	}
518 	
519 	
520 	/**
521 	 * <p>
522 	 * Writes the child nodes into the stream.
523 	 * </p>
524 	 *
525 	 * @param out
526 	 *            The stream.
527 	 * @throws IOException
528 	 *             If an I/O error occurs.
529 	 */
530 	private void writeNodes(TarArchiveOutputStream out) throws IOException {
531 		List<DataStoreNode> nodes;
532 
533 
534 		// --- Get sorted nodes ---
535 		nodes = this.getChildNodes(true);
536 
537 		// --- Writes nodes ---
538 		for (DataStoreNode node : nodes) {
539 			node.write(out);
540 		}
541 	}
542 
543 
544 	/**
545 	 * <p>
546 	 * Creates the file hashes for all files within the directory and its
547 	 * sub-directories (if this node represents a directory) or for the file (if
548 	 * this node represents a file).
549 	 * </p>
550 	 * <p>
551 	 * Symbolic links are ignored. No hash will be created for them.
552 	 * </p>
553 	 *
554 	 * @param digest
555 	 *            The digest used for creating the hashes.
556 	 * @return The hashes.
557 	 * @throws IOException
558 	 *             If an I/O error occurs.
559 	 */
560 	public List<FileHash> createFileHashes(MessageDigest digest) throws IOException {
561 		List<FileHash> hashes;
562 		List<DataStoreNode> nodes;
563 		
564 		
565 		if (digest == null)
566 			throw new IllegalArgumentException("Argument md is null.");
567 		
568 		hashes = new ArrayList<>();
569 		// --- Create the hash for the content (if the node is a file) ---
570 		if (this.source != null) {
571 			hashes.add(this.createFileHash(digest));
572 		}
573 		// --- Otherwise process the child nodes (if the node is a directory and we have child nodes) ---
574 		else if ((this.childNodes != null) && !this.childNodes.isEmpty()) {
575 			nodes = this.getChildNodes(true);
576 			for (DataStoreNode node : nodes) {
577 				hashes.addAll(node.createFileHashes(digest));
578 			}
579 		}
580 
581 		return hashes;
582 	}
583 
584 
585 	/**
586 	 * <p>
587 	 * Creates the file hash for this node.
588 	 * </p>
589 	 *
590 	 * @param digest
591 	 *            The digest.
592 	 * @return The file hash.
593 	 * @throws IOException
594 	 *             If an I/O error occurs.
595 	 */
596 	private FileHash createFileHash(MessageDigest digest) throws IOException {
597 		FileHash fileHash;
598 		DataProducer producer;
599 		DataConsumer consumer;
600 		byte[] hash;
601 		String path;
602 
603 
604 		// --- Create hash ---
605 		producer = Streams.createProducer(this.source);
606 		consumer = new DataDigestConsumer(digest, "digest");
607 		digest.reset();
608 		Streams.transfer(producer, consumer);
609 		hash = digest.digest();
610 
611 		// --- Reset the source (if supported) ---
612 		if (this.source.isResettable()) {
613 			this.source.reset();
614 		}
615 		
616 		// --- Create file hash ---
617 		path = this.getPath();
618 		fileHash = new FileHashImpl(this.name, path, hash);
619 		return fileHash;
620 	}
621 
622 
623 }