View Javadoc
1   /*
2    * Copyright (c) 2020 bahlef.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v2.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v20.html
7    * Contributors:
8    * bahlef - initial API and implementation and/or initial documentation
9    */
10  
11  package de.funfried.netbeans.plugins.external.formatter.eclipse.xml;
12  
13  import java.io.FileInputStream;
14  import java.io.IOException;
15  import java.net.URL;
16  import java.nio.file.Path;
17  import java.nio.file.Paths;
18  import java.util.HashMap;
19  import java.util.LinkedHashMap;
20  import java.util.Map;
21  import java.util.Properties;
22  import java.util.logging.Level;
23  import java.util.logging.Logger;
24  import java.util.prefs.Preferences;
25  import java.util.stream.Collectors;
26  import java.util.stream.Stream;
27  
28  import javax.swing.text.Document;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.netbeans.api.annotations.common.CheckForNull;
32  import org.netbeans.api.annotations.common.NonNull;
33  import org.netbeans.api.project.FileOwnerQuery;
34  import org.netbeans.api.project.Project;
35  import org.netbeans.modules.editor.NbEditorUtilities;
36  import org.openide.filesystems.FileObject;
37  
38  import de.funfried.netbeans.plugins.external.formatter.eclipse.mechanic.WorkspaceMechanicConfigParser;
39  import de.funfried.netbeans.plugins.external.formatter.exceptions.CannotLoadConfigurationException;
40  import de.funfried.netbeans.plugins.external.formatter.exceptions.ConfigReadException;
41  import de.funfried.netbeans.plugins.external.formatter.exceptions.ProfileNotFoundException;
42  
43  /**
44   *
45   * @author bahlef
46   */
47  public class EclipseFormatterUtils {
48  	/** {@link Logger} of this class. */
49  	private static final Logger log = Logger.getLogger(EclipseFormatterUtils.class.getName());
50  
51  	/** EPF file extension */
52  	public static final String EPF_FILE_EXTENSION = ".epf";
53  
54  	/** XML file extension */
55  	public static final String XML_FILE_EXTENSION = ".xml";
56  
57  	/**
58  	 * Private constructor due to static methods only.
59  	 */
60  	private EclipseFormatterUtils() {
61  	}
62  
63  	/**
64  	 * Returns the Eclipse formatter file for the given {@link Document} from the given {@link Preferences}.
65  	 * If the value behind {@code useProjectPrefsKey} is {@code true} in the given {@link Preferences}, it
66  	 * will be automatically checked if there is a project specific formatter configuration file available.
67  	 *
68  	 * @param preferences the {@link Preferences} where to load from
69  	 * @param document the {@link Document}
70  	 * @param configFileLocationKey the preferences key for the configuration file location
71  	 * @param useProjectPrefsKey the preferences key whether to use project preferences
72  	 * @param projectPrefFile the expected Eclipse project specific formatter configuration file name
73  	 *
74  	 * @return the Eclipse formatter file for the given {@link Document} from the given {@link Preferences}.
75  	 *         If the value behind {@code useProjectPrefsKey} is {@code true} in the given {@link Preferences},
76  	 *         it will be automatically checked if there is a project specific formatter configuration file
77  	 *         available.
78  	 */
79  	public static String getEclipseFormatterFile(Preferences preferences, Document document, String configFileLocationKey, String useProjectPrefsKey, String projectPrefFile) {
80  		String formatterFilePref = null;
81  		if (preferences.getBoolean(useProjectPrefsKey, true)) {
82  			//use ${projectdir}/.settings/projectPrefFile, if activated in options
83  			formatterFilePref = getFormatterFileFromProjectConfiguration(document, ".settings/" + projectPrefFile);
84  		}
85  
86  		if (StringUtils.isBlank(formatterFilePref)) {
87  			formatterFilePref = preferences.get(configFileLocationKey, null);
88  			if (StringUtils.isNotBlank(formatterFilePref)) {
89  				Path formatterFilePath = Paths.get(formatterFilePref);
90  				if (!formatterFilePath.isAbsolute()) {
91  					formatterFilePref = getFormatterFileFromProjectConfiguration(document, formatterFilePref);
92  				}
93  			}
94  		}
95  
96  		return formatterFilePref;
97  	}
98  
99  	/**
100 	 * Checks for a project specific Eclipse formatter configuration for the given {@link Document} and returns
101 	 * the file location if found, otherwise {@code null}.
102 	 *
103 	 * @param document the {@link Document}
104 	 * @param relativeFileName the relative configuration file name
105 	 *
106 	 * @return project specific Eclipse formatter configuration for the given {@link Document} if existent,
107 	 *         otherwise {@code null}
108 	 */
109 	@CheckForNull
110 	private static String getFormatterFileFromProjectConfiguration(Document document, String relativeFileName) {
111 		FileObject fileForDocument = NbEditorUtilities.getFileObject(document);
112 		if (null != fileForDocument) {
113 			Project project = FileOwnerQuery.getOwner(fileForDocument);
114 			if (null != project) {
115 				FileObject projectDirectory = project.getProjectDirectory();
116 				FileObject preferenceFile = projectDirectory.getFileObject(StringUtils.replace(relativeFileName, "\\", "/"));
117 				if (null != preferenceFile) {
118 					return preferenceFile.getPath();
119 				}
120 			}
121 		}
122 
123 		return null;
124 	}
125 
126 	/**
127 	 * Returns {@code true} if the given {@code filename} ends with the given {@code projectPrefFile}.
128 	 *
129 	 * @param filename the filename to check
130 	 * @param projectPrefFile the expected Eclipse project specific formatter configuration file name
131 	 *
132 	 * @return {@code true} if the given {@code filename} ends with {@code org.eclipse.jdt.core.prefs},
133 	 *         otherwise {@code false}
134 	 */
135 	public static boolean isProjectSetting(String filename, String projectPrefFile) {
136 		return filename != null && StringUtils.isNotBlank(projectPrefFile) && filename.endsWith(projectPrefFile);
137 	}
138 
139 	/**
140 	 * Returns {@code true} if the given {@code filename} ends with the workspace mechanic file extension epf.
141 	 *
142 	 * @param filename the filename to check
143 	 *
144 	 * @return {@code true} if the given {@code filename} ends with the workspace mechanic file extension epf,
145 	 *         otherwise {@code false}
146 	 */
147 	public static boolean isWorkspaceMechanicFile(String filename) {
148 		return filename != null && filename.endsWith(EPF_FILE_EXTENSION);
149 	}
150 
151 	/**
152 	 * Returns {@code true} if the given {@code filename} ends with the XML file extension.
153 	 *
154 	 * @param filename the filename to check
155 	 *
156 	 * @return {@code true} if the given {@code filename} ends with the XML file extension, otherwise
157 	 *         {@code false}
158 	 */
159 	public static boolean isXMLConfigurationFile(String filename) {
160 		return filename != null && filename.endsWith(XML_FILE_EXTENSION);
161 	}
162 
163 	/**
164 	 * Parses the configuration parameters from the given {@code formatterProfile} of the
165 	 * given {@code formatterFile} and returns it as a {@link Map} containing the
166 	 * configuration as key value pairs.
167 	 *
168 	 * @param formatterFile the path to the formatter configuration file
169 	 * @param formatterProfile the name of the formatter configuration profile
170 	 * @param defaultProperties the default properties
171 	 * @param additionalProperties optional additional properties
172 	 * @param workspaceMechanicPrefix the workspace mechanic prefix
173 	 * @param projectPrefFile the expected Eclipse project specific formatter configuration file name
174 	 *
175 	 * @return a {@link Map} containing the configuration as key value pairs
176 	 *
177 	 * @throws ConfigReadException if there is an issue parsing the formatter configuration
178 	 * @throws ProfileNotFoundException if the given {@code profile} could not be found
179 	 * @throws CannotLoadConfigurationException if there is any issue accessing or reading the formatter configuration
180 	 */
181 	public static Map<String, String> parseConfig(String formatterFile, String formatterProfile, Map<String, String> defaultProperties, Map<String, String> additionalProperties,
182 			String workspaceMechanicPrefix, String projectPrefFile) throws ProfileNotFoundException, ConfigReadException, CannotLoadConfigurationException {
183 		Map<String, String> allConfig = new HashMap<>();
184 		try {
185 			Map<String, String> configFromFile;
186 			if (EclipseFormatterUtils.isWorkspaceMechanicFile(formatterFile)) {
187 				configFromFile = WorkspaceMechanicConfigParser.readPropertiesFromConfiguration(formatterFile, workspaceMechanicPrefix);
188 			} else if (EclipseFormatterUtils.isXMLConfigurationFile(formatterFile)) {
189 				configFromFile = ConfigReader.getProfileSettings(ConfigReader.readContentFromFilePath(formatterFile), formatterProfile);
190 			} else if (EclipseFormatterUtils.isProjectSetting(formatterFile, projectPrefFile)) {
191 				configFromFile = EclipseFormatterUtils.readPropertiesFromConfigurationFile(formatterFile);
192 			} else {
193 				configFromFile = new LinkedHashMap<>();
194 			}
195 
196 			allConfig.putAll(defaultProperties);
197 			allConfig.putAll(configFromFile);
198 
199 			if (additionalProperties != null) {
200 				allConfig.putAll(additionalProperties);
201 			}
202 		} catch (ConfigReadException | ProfileNotFoundException ex) {
203 			log.log(Level.WARNING, "Could not load configuration: " + formatterFile, ex);
204 
205 			throw ex;
206 		} catch (Exception ex) {
207 			log.log(Level.WARNING, "Could not load configuration: " + formatterFile, ex);
208 
209 			throw new CannotLoadConfigurationException(ex);
210 		}
211 
212 		return allConfig;
213 	}
214 
215 	/**
216 	 * Parses and returns properties of the given {@code filePath} into a key value {@link Map}.
217 	 *
218 	 * @param filePath a configuration file path
219 	 *
220 	 * @return properties of the given {@code file} as a key value {@link Map}
221 	 *
222 	 * @throws IOException if there is an issue accessing the given configuration file
223 	 */
224 	@NonNull
225 	public static Map<String, String> readPropertiesFromConfigurationFile(String filePath) throws IOException {
226 		Properties properties = new Properties();
227 
228 		try {
229 			URL url = new URL(filePath);
230 
231 			properties.load(url.openStream());
232 		} catch (IOException ex) {
233 			log.log(Level.FINEST, "Could not read file via URL, fallback to local file reading", ex);
234 
235 			try (FileInputStream is = new FileInputStream(filePath)) {
236 				properties.load(is);
237 			}
238 		}
239 
240 		return EclipseFormatterUtils.toMap(properties, null);
241 	}
242 
243 	/**
244 	 * Collect the given properties into a map and optionall filter the property keys by the given optional prefix.
245 	 *
246 	 * @param properties The {@link Properties} to filter and collect.
247 	 * @param prefix An optional prefix to filter the keys.
248 	 *
249 	 * @return A map containing the keys and their respective values
250 	 */
251 	public static Map<String, String> toMap(Properties properties, String prefix) {
252 		Stream<Object> stream = properties.keySet().stream();
253 		if (StringUtils.isNotBlank(prefix)) {
254 			return stream.filter(key -> ((String) key).startsWith(prefix)).collect(Collectors.toMap(key -> ((String) key).substring(prefix.length()), key -> properties.getProperty((String) key)));
255 		}
256 
257 		return stream.collect(Collectors.toMap(key -> (String) key, key -> properties.getProperty((String) key)));
258 	}
259 }