FileElement.java

/*
 * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
 *
 * 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.internal.diffmergetool;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

import org.eclipse.jgit.diff.DiffEntry;

/**
 * The element used as left or right file for compare.
 *
 */
public class FileElement {

	/**
	 * The file element type.
	 *
	 */
	public enum Type {
		/**
		 * The local file element (ours).
		 */
		LOCAL,
		/**
		 * The remote file element (theirs).
		 */
		REMOTE,
		/**
		 * The merged file element (path in worktree).
		 */
		MERGED,
		/**
		 * The base file element (of ours and theirs).
		 */
		BASE,
		/**
		 * The backup file element (copy of merged / conflicted).
		 */
		BACKUP
	}

	private final String path;

	private final Type type;

	private final File workDir;

	private InputStream stream;

	private File tempFile;

	/**
	 * Creates file element for path.
	 *
	 * @param path
	 *            the file path
	 * @param type
	 *            the element type
	 */
	public FileElement(String path, Type type) {
		this(path, type, null);
	}

	/**
	 * Creates file element for path.
	 *
	 * @param path
	 *            the file path
	 * @param type
	 *            the element type
	 * @param workDir
	 *            the working directory of the path (can be null, then current
	 *            working dir is used)
	 */
	public FileElement(String path, Type type, File workDir) {
		this(path, type, workDir, null);
	}

	/**
	 * @param path
	 *            the file path
	 * @param type
	 *            the element type
	 * @param workDir
	 *            the working directory of the path (can be null, then current
	 *            working dir is used)
	 * @param stream
	 *            the object stream to load and write on demand, @see getFile(),
	 *            to tempFile once (can be null)
	 */
	public FileElement(String path, Type type, File workDir,
			InputStream stream) {
		this.path = path;
		this.type = type;
		this.workDir = workDir;
		this.stream = stream;
	}

	/**
	 * @return the file path
	 */
	public String getPath() {
		return path;
	}

	/**
	 * @return the element type
	 */
	public Type getType() {
		return type;
	}

	/**
	 * Return
	 * <ul>
	 * <li>a temporary file if already created and stream is not valid</li>
	 * <li>OR a real file from work tree: if no temp file was created (@see
	 * createTempFile()) and if no stream was set</li>
	 * <li>OR an empty temporary file if path is "/dev/null"</li>
	 * <li>OR a temporary file with stream content if stream is valid (not
	 * null); stream is closed and invalidated (set to null) after write to temp
	 * file, so stream is used only once during first call!</li>
	 * </ul>
	 *
	 * @return the object stream
	 * @throws IOException
	 */
	public File getFile() throws IOException {
		// if we have already temp file and no stream
		// then just return this temp file (it was filled from outside)
		if ((tempFile != null) && (stream == null)) {
			return tempFile;
		}
		File file = new File(workDir, path);
		// if we have a stream or file is missing (path is "/dev/null")
		// then optionally create temporary file and fill it with stream content
		if ((stream != null) || isNullPath()) {
			if (tempFile == null) {
				tempFile = getTempFile(file, type.name(), null);
			}
			if (stream != null) {
				copyFromStream(tempFile, stream);
			}
			// invalidate the stream, because it is used once
			stream = null;
			return tempFile;
		}
		return file;
	}

	/**
	 * Check if path id "/dev/null"
	 *
	 * @return true if path is "/dev/null"
	 */
	public boolean isNullPath() {
		return path.equals(DiffEntry.DEV_NULL);
	}

	/**
	 * Create temporary file in given or system temporary directory.
	 *
	 * @param directory
	 *            the directory for the file (can be null); if null system
	 *            temporary directory is used
	 * @return temporary file in directory or in the system temporary directory
	 * @throws IOException
	 */
	public File createTempFile(File directory) throws IOException {
		if (tempFile == null) {
			tempFile = getTempFile(new File(path), type.name(), directory);
		}
		return tempFile;
	}

	/**
	 * Delete and invalidate temporary file if necessary.
	 */
	public void cleanTemporaries() {
		if (tempFile != null && tempFile.exists()) {
			tempFile.delete();
		}
		tempFile = null;
	}

	/**
	 * Replace variable in input.
	 *
	 * @param input
	 *            the input string
	 * @return the replaced input string
	 * @throws IOException
	 */
	public String replaceVariable(String input) throws IOException {
		return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$
	}

	/**
	 * Add variable to environment map.
	 *
	 * @param env
	 *            the environment where this element should be added
	 * @throws IOException
	 */
	public void addToEnv(Map<String, String> env) throws IOException {
		env.put(type.name(), getFile().getPath());
	}

	private static File getTempFile(final File file, final String midName,
			final File workingDir) throws IOException {
		String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
		// TODO: avoid long random file name (number generated by
		// createTempFile)
		return File.createTempFile(
				fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
				fileNameAndExtension[1], workingDir);
	}

	private static void copyFromStream(final File file,
			final InputStream stream)
			throws IOException, FileNotFoundException {
		try (OutputStream outStream = new FileOutputStream(file)) {
			int read = 0;
			byte[] bytes = new byte[8 * 1024];
			while ((read = stream.read(bytes)) != -1) {
				outStream.write(bytes, 0, read);
			}
		} finally {
			// stream can only be consumed once --> close it and invalidate
			stream.close();
		}
	}

	private static String[] splitBaseFileNameAndExtension(File file) {
		String[] result = new String[2];
		result[0] = file.getName();
		result[1] = ""; //$NON-NLS-1$
		int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
		// if "." was found (>-1) and last-index is not first char (>0), then
		// split (same behavior like cgit)
		if (idx > 0) {
			result[1] = result[0].substring(idx, result[0].length());
			result[0] = result[0].substring(0, idx);
		}
		return result;
	}

}