ObjectBuilder.java

/*
 * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.lib;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Objects;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.References;

/**
 * Common base class for {@link CommitBuilder} and {@link TagBuilder}.
 *
 * @since 5.11
 */
public abstract class ObjectBuilder {

	/** Byte representation of "encoding". */
	private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$

	private PersonIdent author;

	private GpgSignature gpgSignature;

	private String message;

	private Charset encoding = StandardCharsets.UTF_8;

	/**
	 * Retrieves the author of this object.
	 *
	 * @return the author of this object, or {@code null} if not set yet
	 */
	protected PersonIdent getAuthor() {
		return author;
	}

	/**
	 * Sets the author (name, email address, and date) of this object.
	 *
	 * @param newAuthor
	 *            the new author, must be non-{@code null}
	 */
	protected void setAuthor(PersonIdent newAuthor) {
		author = Objects.requireNonNull(newAuthor);
	}

	/**
	 * Sets the GPG signature of this object.
	 * <p>
	 * Note, the signature set here will change the payload of the object, i.e.
	 * the output of {@link #build()} will include the signature. Thus, the
	 * typical flow will be:
	 * <ol>
	 * <li>call {@link #build()} without a signature set to obtain payload</li>
	 * <li>create {@link GpgSignature} from payload</li>
	 * <li>set {@link GpgSignature}</li>
	 * </ol>
	 * </p>
	 *
	 * @param gpgSignature
	 *            the signature to set or {@code null} to unset
	 * @since 5.3
	 */
	public void setGpgSignature(@Nullable GpgSignature gpgSignature) {
		this.gpgSignature = gpgSignature;
	}

	/**
	 * Retrieves the GPG signature of this object.
	 *
	 * @return the GPG signature of this object, or {@code null} if the object
	 *         is not signed
	 * @since 5.3
	 */
	@Nullable
	public GpgSignature getGpgSignature() {
		return gpgSignature;
	}

	/**
	 * Retrieves the complete message of the object.
	 *
	 * @return the complete message; can be {@code null}.
	 */
	@Nullable
	public String getMessage() {
		return message;
	}

	/**
	 * Sets the message (commit message, or message of an annotated tag).
	 *
	 * @param message
	 *            the message.
	 */
	public void setMessage(@Nullable String message) {
		this.message = message;
	}

	/**
	 * Retrieves the encoding that should be used for the message text.
	 *
	 * @return the encoding that should be used for the message text.
	 */
	@NonNull
	public Charset getEncoding() {
		return encoding;
	}

	/**
	 * Sets the encoding for the object message.
	 *
	 * @param encoding
	 *            the encoding to use.
	 */
	public void setEncoding(@NonNull Charset encoding) {
		this.encoding = encoding;
	}

	/**
	 * Format this builder's state as a git object.
	 *
	 * @return this object in the canonical git format, suitable for storage in
	 *         a repository.
	 * @throws java.io.UnsupportedEncodingException
	 *             the encoding specified by {@link #getEncoding()} is not
	 *             supported by this Java runtime.
	 */
	@NonNull
	public abstract byte[] build() throws UnsupportedEncodingException;

	/**
	 * Writes signature to output as per <a href=
	 * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
	 * header</a>.
	 * <p>
	 * CRLF and CR will be sanitized to LF and signature will have a hanging
	 * indent of one space starting with line two. A trailing line break is
	 * <em>not</em> written; the caller is supposed to terminate the GPG
	 * signature header by writing a single newline.
	 * </p>
	 *
	 * @param in
	 *            signature string with line breaks
	 * @param out
	 *            output stream
	 * @param enforceAscii
	 *            whether to throw {@link IllegalArgumentException} if non-ASCII
	 *            characters are encountered
	 * @throws IOException
	 *             thrown by the output stream
	 * @throws IllegalArgumentException
	 *             if the signature string contains non 7-bit ASCII chars and
	 *             {@code enforceAscii == true}
	 */
	static void writeMultiLineHeader(@NonNull String in,
			@NonNull OutputStream out, boolean enforceAscii)
			throws IOException, IllegalArgumentException {
		int length = in.length();
		for (int i = 0; i < length; ++i) {
			char ch = in.charAt(i);
			switch (ch) {
			case '\r':
				if (i + 1 < length && in.charAt(i + 1) == '\n') {
					++i;
				}
				if (i + 1 < length) {
					out.write('\n');
					out.write(' ');
				}
				break;
			case '\n':
				if (i + 1 < length) {
					out.write('\n');
					out.write(' ');
				}
				break;
			default:
				// sanity check
				if (ch > 127 && enforceAscii)
					throw new IllegalArgumentException(MessageFormat
							.format(JGitText.get().notASCIIString, in));
				out.write(ch);
				break;
			}
		}
	}

	/**
	 * Writes an "encoding" header.
	 *
	 * @param encoding
	 *            to write
	 * @param out
	 *            to write to
	 * @throws IOException
	 *             if writing fails
	 */
	static void writeEncoding(@NonNull Charset encoding,
			@NonNull OutputStream out) throws IOException {
		if (!References.isSameObject(encoding, UTF_8)) {
			out.write(hencoding);
			out.write(' ');
			out.write(Constants.encodeASCII(encoding.name()));
			out.write('\n');
		}
	}
}