EclipseFormatterUtils.java

/*
 * Copyright (c) 2020 bahlef.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * Contributors:
 * bahlef - initial API and implementation and/or initial documentation
 */

package de.funfried.netbeans.plugins.external.formatter.eclipse.xml;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.swing.text.Document;

import org.apache.commons.lang3.StringUtils;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.openide.filesystems.FileObject;

import de.funfried.netbeans.plugins.external.formatter.eclipse.mechanic.WorkspaceMechanicConfigParser;
import de.funfried.netbeans.plugins.external.formatter.exceptions.CannotLoadConfigurationException;
import de.funfried.netbeans.plugins.external.formatter.exceptions.ConfigReadException;
import de.funfried.netbeans.plugins.external.formatter.exceptions.ProfileNotFoundException;

/**
 *
 * @author bahlef
 */
public class EclipseFormatterUtils {
	/** {@link Logger} of this class. */
	private static final Logger log = Logger.getLogger(EclipseFormatterUtils.class.getName());

	/** EPF file extension */
	public static final String EPF_FILE_EXTENSION = ".epf";

	/** XML file extension */
	public static final String XML_FILE_EXTENSION = ".xml";

	/**
	 * Private constructor due to static methods only.
	 */
	private EclipseFormatterUtils() {
	}

	/**
	 * Returns the Eclipse formatter file for the given {@link Document} from the given {@link Preferences}.
	 * If the value behind {@code useProjectPrefsKey} is {@code true} in the given {@link Preferences}, it
	 * will be automatically checked if there is a project specific formatter configuration file available.
	 *
	 * @param preferences the {@link Preferences} where to load from
	 * @param document the {@link Document}
	 * @param configFileLocationKey the preferences key for the configuration file location
	 * @param useProjectPrefsKey the preferences key whether to use project preferences
	 * @param projectPrefFile the expected Eclipse project specific formatter configuration file name
	 *
	 * @return the Eclipse formatter file for the given {@link Document} from the given {@link Preferences}.
	 *         If the value behind {@code useProjectPrefsKey} is {@code true} in the given {@link Preferences},
	 *         it will be automatically checked if there is a project specific formatter configuration file
	 *         available.
	 */
	public static String getEclipseFormatterFile(Preferences preferences, Document document, String configFileLocationKey, String useProjectPrefsKey, String projectPrefFile) {
		String formatterFilePref = null;
		if (preferences.getBoolean(useProjectPrefsKey, true)) {
			//use ${projectdir}/.settings/projectPrefFile, if activated in options
			formatterFilePref = getFormatterFileFromProjectConfiguration(document, ".settings/" + projectPrefFile);
		}

		if (StringUtils.isBlank(formatterFilePref)) {
			formatterFilePref = preferences.get(configFileLocationKey, null);
			if (StringUtils.isNotBlank(formatterFilePref)) {
				Path formatterFilePath = Paths.get(formatterFilePref);
				if (!formatterFilePath.isAbsolute()) {
					formatterFilePref = getFormatterFileFromProjectConfiguration(document, formatterFilePref);
				}
			}
		}

		return formatterFilePref;
	}

	/**
	 * Checks for a project specific Eclipse formatter configuration for the given {@link Document} and returns
	 * the file location if found, otherwise {@code null}.
	 *
	 * @param document the {@link Document}
	 * @param relativeFileName the relative configuration file name
	 *
	 * @return project specific Eclipse formatter configuration for the given {@link Document} if existent,
	 *         otherwise {@code null}
	 */
	@CheckForNull
	private static String getFormatterFileFromProjectConfiguration(Document document, String relativeFileName) {
		FileObject fileForDocument = NbEditorUtilities.getFileObject(document);
		if (null != fileForDocument) {
			Project project = FileOwnerQuery.getOwner(fileForDocument);
			if (null != project) {
				FileObject projectDirectory = project.getProjectDirectory();
				FileObject preferenceFile = projectDirectory.getFileObject(StringUtils.replace(relativeFileName, "\\", "/"));
				if (null != preferenceFile) {
					return preferenceFile.getPath();
				}
			}
		}

		return null;
	}

	/**
	 * Returns {@code true} if the given {@code filename} ends with the given {@code projectPrefFile}.
	 *
	 * @param filename the filename to check
	 * @param projectPrefFile the expected Eclipse project specific formatter configuration file name
	 *
	 * @return {@code true} if the given {@code filename} ends with {@code org.eclipse.jdt.core.prefs},
	 *         otherwise {@code false}
	 */
	public static boolean isProjectSetting(String filename, String projectPrefFile) {
		return filename != null && StringUtils.isNotBlank(projectPrefFile) && filename.endsWith(projectPrefFile);
	}

	/**
	 * Returns {@code true} if the given {@code filename} ends with the workspace mechanic file extension epf.
	 *
	 * @param filename the filename to check
	 *
	 * @return {@code true} if the given {@code filename} ends with the workspace mechanic file extension epf,
	 *         otherwise {@code false}
	 */
	public static boolean isWorkspaceMechanicFile(String filename) {
		return filename != null && filename.endsWith(EPF_FILE_EXTENSION);
	}

	/**
	 * Returns {@code true} if the given {@code filename} ends with the XML file extension.
	 *
	 * @param filename the filename to check
	 *
	 * @return {@code true} if the given {@code filename} ends with the XML file extension, otherwise
	 *         {@code false}
	 */
	public static boolean isXMLConfigurationFile(String filename) {
		return filename != null && filename.endsWith(XML_FILE_EXTENSION);
	}

	/**
	 * Parses the configuration parameters from the given {@code formatterProfile} of the
	 * given {@code formatterFile} and returns it as a {@link Map} containing the
	 * configuration as key value pairs.
	 *
	 * @param formatterFile the path to the formatter configuration file
	 * @param formatterProfile the name of the formatter configuration profile
	 * @param defaultProperties the default properties
	 * @param additionalProperties optional additional properties
	 * @param workspaceMechanicPrefix the workspace mechanic prefix
	 * @param projectPrefFile the expected Eclipse project specific formatter configuration file name
	 *
	 * @return a {@link Map} containing the configuration as key value pairs
	 *
	 * @throws ConfigReadException if there is an issue parsing the formatter configuration
	 * @throws ProfileNotFoundException if the given {@code profile} could not be found
	 * @throws CannotLoadConfigurationException if there is any issue accessing or reading the formatter configuration
	 */
	public static Map<String, String> parseConfig(String formatterFile, String formatterProfile, Map<String, String> defaultProperties, Map<String, String> additionalProperties,
			String workspaceMechanicPrefix, String projectPrefFile) throws ProfileNotFoundException, ConfigReadException, CannotLoadConfigurationException {
		Map<String, String> allConfig = new HashMap<>();
		try {
			Map<String, String> configFromFile;
			if (EclipseFormatterUtils.isWorkspaceMechanicFile(formatterFile)) {
				configFromFile = WorkspaceMechanicConfigParser.readPropertiesFromConfiguration(formatterFile, workspaceMechanicPrefix);
			} else if (EclipseFormatterUtils.isXMLConfigurationFile(formatterFile)) {
				configFromFile = ConfigReader.getProfileSettings(ConfigReader.readContentFromFilePath(formatterFile), formatterProfile);
			} else if (EclipseFormatterUtils.isProjectSetting(formatterFile, projectPrefFile)) {
				configFromFile = EclipseFormatterUtils.readPropertiesFromConfigurationFile(formatterFile);
			} else {
				configFromFile = new LinkedHashMap<>();
			}

			allConfig.putAll(defaultProperties);
			allConfig.putAll(configFromFile);

			if (additionalProperties != null) {
				allConfig.putAll(additionalProperties);
			}
		} catch (ConfigReadException | ProfileNotFoundException ex) {
			log.log(Level.WARNING, "Could not load configuration: " + formatterFile, ex);

			throw ex;
		} catch (Exception ex) {
			log.log(Level.WARNING, "Could not load configuration: " + formatterFile, ex);

			throw new CannotLoadConfigurationException(ex);
		}

		return allConfig;
	}

	/**
	 * Parses and returns properties of the given {@code filePath} into a key value {@link Map}.
	 *
	 * @param filePath a configuration file path
	 *
	 * @return properties of the given {@code file} as a key value {@link Map}
	 *
	 * @throws IOException if there is an issue accessing the given configuration file
	 */
	@NonNull
	public static Map<String, String> readPropertiesFromConfigurationFile(String filePath) throws IOException {
		Properties properties = new Properties();

		try {
			URL url = new URL(filePath);

			properties.load(url.openStream());
		} catch (IOException ex) {
			log.log(Level.FINEST, "Could not read file via URL, fallback to local file reading", ex);

			try (FileInputStream is = new FileInputStream(filePath)) {
				properties.load(is);
			}
		}

		return EclipseFormatterUtils.toMap(properties, null);
	}

	/**
	 * Collect the given properties into a map and optionall filter the property keys by the given optional prefix.
	 *
	 * @param properties The {@link Properties} to filter and collect.
	 * @param prefix An optional prefix to filter the keys.
	 *
	 * @return A map containing the keys and their respective values
	 */
	public static Map<String, String> toMap(Properties properties, String prefix) {
		Stream<Object> stream = properties.keySet().stream();
		if (StringUtils.isNotBlank(prefix)) {
			return stream.filter(key -> ((String) key).startsWith(prefix)).collect(Collectors.toMap(key -> ((String) key).substring(prefix.length()), key -> properties.getProperty((String) key)));
		}

		return stream.collect(Collectors.toMap(key -> (String) key, key -> properties.getProperty((String) key)));
	}
}