ArchiveResource.java

/*-
 * #%L
 * io.earcam.instrumental.archive
 * %%
 * Copyright (C) 2018 earcam
 * %%
 * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
 * 
 * You <b>must</b> choose to accept, in full - any individual or combination of 
 * the following licenses:
 * <ul>
 * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
 * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
 * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
 * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
 * </ul>
 * #L%
 */
package io.earcam.instrumental.archive;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Objects;

import io.earcam.unexceptional.Exceptional;
import io.earcam.utilitarian.charstar.CharSequences;
import io.earcam.utilitarian.io.IoStreams;

/**
 * <p>
 * ArchiveResource class.
 * </p>
 * <p>
 * Callers are expected to close {@link InputStream}s where read. Otherwise, reliance is
 * placed upon the {@link InputStream} implementation to {@link InputStream#close()} itself in
 * it's own {@code finalize} method (e.g. where the underlying maintains file handles).
 * </p>
 *
 */
public class ArchiveResource {

	private String name;
	private byte[] contents;
	private InputStream inputStream;


	/**
	 * <p>
	 * Constructor for ArchiveResource.
	 * </p>
	 *
	 * @param name a {@link java.lang.String} object.
	 * @param contents an array of {@link byte} objects.
	 */
	public ArchiveResource(String name, byte[] contents)
	{
		this.name = (name.charAt(0) == '/') ? name.substring(1) : name;
		this.contents = contents;
		this.inputStream = new ByteArrayInputStream(contents);
	}


	/**
	 * <p>
	 * Constructor for ArchiveResource.
	 * </p>
	 *
	 * @param name a {@link java.lang.String} object.
	 * @param inputStream a {@link java.io.InputStream} object.
	 */
	public ArchiveResource(String name, InputStream inputStream)
	{
		this.name = name;
		this.inputStream = inputStream;
	}


	public static ArchiveResource rename(String aka, ArchiveResource formerly)
	{
		return formerly.isInputStreamBacked() ? new ArchiveResource(aka, formerly.inputStream())
				: new ArchiveResource(aka, formerly.bytes());
	}


	/**
	 * <p>
	 * Compares the {@code name} argument with this resource's name. A leading
	 * slash '/', if present, shall be removed.
	 * </p>
	 * 
	 * @param name a prospective resource name.
	 * @return {@code true} IFF the supplied argument matches this name.
	 *
	 * @see #sameName(CharSequence, CharSequence)
	 */
	public boolean sameName(CharSequence name)
	{
		return sameName(this.name, name);
	}


	static boolean sameName(CharSequence a, CharSequence b)
	{
		return CharSequences.same(trimLeadingSlash(a), trimLeadingSlash(b));
	}


	private static CharSequence trimLeadingSlash(CharSequence text)
	{
		return (text.charAt(0) == '/') ? text.subSequence(1, text.length()) : text;
	}


	@Override
	public boolean equals(Object other)
	{
		return other instanceof ArchiveResource && equals((ArchiveResource) other);
	}


	/**
	 * <p>
	 * equals.
	 * </p>
	 *
	 * @param that a {@link io.earcam.instrumental.archive.ArchiveResource} object.
	 * @return a boolean.
	 */
	public boolean equals(ArchiveResource that)
	{
		return that != null
				&& Objects.equals(that.name(), this.name);
	}


	@Override
	public int hashCode()
	{
		return name.hashCode();
	}


	/**
	 * Writes the underlying rep - which may be an {@link java.io.InputStream} or {@code byte[]}
	 *
	 * @param output a {@link java.io.OutputStream} object.
	 * @throws UncheckedIOException if an {@link java.io.IOException} is thrown
	 */
	public void write(OutputStream output)
	{
		if(isInputStreamBacked()) {
			IoStreams.transfer(inputStream, output);
		} else {
			Exceptional.accept(output::write, contents);
		}
	}


	/**
	 * <p>
	 * isInputStreamBacked.
	 * </p>
	 *
	 * @return a boolean.
	 */
	public boolean isInputStreamBacked()
	{
		return contents == null;
	}


	/**
	 * <p>
	 * name.
	 * </p>
	 *
	 * @return a {@link java.lang.String} object.
	 */
	public String name()
	{
		return name;
	}


	public String extension()
	{
		int index = name.lastIndexOf('.');
		return (index == -1) ? "" : name.substring(index);
	}


	/**
	 * <p>
	 * bytes.
	 * </p>
	 *
	 * @return an array of {@link byte} objects.
	 */
	public byte[] bytes()
	{
		return isInputStreamBacked() ? drained() : contents;
	}


	private byte[] drained()
	{
		contents = IoStreams.readAllBytes(inputStream);
		Exceptional.run(inputStream::close);
		inputStream = null;
		return contents;
	}


	/**
	 * <p>
	 * inputStream.
	 * </p>
	 *
	 * @return a {@link java.io.InputStream} object.
	 */
	public InputStream inputStream()
	{
		return inputStream;
	}


	/**
	 * <p>
	 * unknownSize.
	 * </p>
	 *
	 * @return a boolean.
	 */
	public boolean unknownSize()
	{
		return isInputStreamBacked();
	}


	/**
	 * <p>
	 * knownSize.
	 * </p>
	 *
	 * @return a long.
	 */
	public long knownSize()
	{
		return isInputStreamBacked() ? -1 : contents.length;
	}


	/**
	 * <p>
	 * pkg.
	 * </p>
	 *
	 * @return a {@link java.lang.String} object.
	 */
	public String pkg()
	{
		int end = name.lastIndexOf('/');
		return ((end == -1) ? "" : name.substring(0, end)).replace('/', '.');
	}


	public boolean isQualifiedClass()
	{
		return isClass() && !"".equals(pkg());
	}


	public boolean isClass()
	{
		return ".class".equals(extension());
	}
}