/*
* Copyright (C) 2014 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.core.configuration;
import static lombok.core.configuration.FileSystemSourceCache.fileToString;
import java.io.File;
import java.io.PrintStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import lombok.ConfigurationKeys;
import lombok.core.LombokApp;
import lombok.core.configuration.ConfigurationParser.Collector;
import org.mangosdk.spi.ProviderFor;
import com.zwitserloot.cmdreader.CmdReader;
import com.zwitserloot.cmdreader.Description;
import com.zwitserloot.cmdreader.Excludes;
import com.zwitserloot.cmdreader.InvalidCommandLineException;
import com.zwitserloot.cmdreader.Mandatory;
import com.zwitserloot.cmdreader.Sequential;
import com.zwitserloot.cmdreader.Shorthand;
@ProviderFor(LombokApp.class)
public class ConfigurationApp extends LombokApp {
private static final URI NO_CONFIG = URI.create("");
private PrintStream out = System.out;
private PrintStream err = System.err;
@Override public String getAppName() {
return "config";
}
@Override public String getAppDescription() {
return "Prints the configurations for the provided paths to standard out.";
}
@Override public List<String> getAppAliases() {
return Arrays.asList("configuration", "config", "conf", "settings");
}
public static class CmdArgs {
@Sequential
@Mandatory(onlyIfNot={"help", "generate"})
@Description("Paths to java files or directories the configuration is to be printed for.")
private List<String> paths = new ArrayList<String>();
@Shorthand("g")
@Excludes("paths")
@Description("Generates a list containing all the available configuration parameters. Add --verbose to print more information.")
boolean generate = false;
@Shorthand("v")
@Description("Displays more information.")
boolean verbose = false;
@Shorthand("k")
@Description("Limit the result to these keys.")
private List<String> key = new ArrayList<String>();
@Shorthand({"h", "?"})
@Description("Shows this help text.")
boolean help = false;
}
@Override public int runApp(List<String> raw) throws Exception {
CmdReader<CmdArgs> reader = CmdReader.of(CmdArgs.class);
CmdArgs args;
try {
args = reader.make(raw.toArray(new String[0]));
if (args.help) {
out.println(reader.generateCommandLineHelp("java -jar lombok.jar configuration"));
return 0;
}
} catch (InvalidCommandLineException e) {
err.println(e.getMessage());
err.println(reader.generateCommandLineHelp("java -jar lombok.jar configuration"));
return 1;
}
ConfigurationKeysLoader.LoaderLoader.loadAllConfigurationKeys();
Collection<ConfigurationKey<?>> keys = checkKeys(args.key);
if (keys == null) return 1;
boolean verbose = args.verbose;
if (args.generate) {
return generate(keys, verbose);
}
return display(keys, verbose, args.paths, !args.key.isEmpty());
}
public ConfigurationApp redirectOutput(PrintStream out, PrintStream err) {
if (out != null) this.out = out;
if (err != null) this.err = err;
return this;
}
public int generate(Collection<ConfigurationKey<?>> keys, boolean verbose) {
for (ConfigurationKey<?> key : keys) {
String keyName = key.getKeyName();
ConfigurationDataType type = key.getType();
String description = key.getDescription();
boolean hasDescription = description != null && !description.isEmpty();
if (!verbose) {
out.println(keyName);
if (hasDescription) {
out.print(" ");
out.println(description);
}
out.println();
continue;
}
out.printf("##%n## Key : %s%n## Type: %s%n", keyName, type);
if (hasDescription) {
out.printf("##%n## %s%n", description);
}
out.printf("##%n## Examples:%n#%n");
out.printf("# clear %s%n", keyName);
String exampleValue = type.getParser().exampleValue();
if (type.isList()) {
out.printf("# %s += %s%n", keyName, exampleValue);
out.printf("# %s -= %s%n", keyName, exampleValue);
} else {
out.printf("# %s = %s%n", keyName, exampleValue);
}
out.printf("#%n%n");
}
if (!verbose) {
out.println("Use --verbose for more information.");
}
return 0;
}
public int display(Collection<ConfigurationKey<?>> keys, boolean verbose, Collection<String> argsPaths, boolean explicitKeys) throws Exception {
TreeMap<URI, Set<String>> sharedDirectories = findSharedDirectories(argsPaths);
if (sharedDirectories == null) return 1;
Set<String> none = sharedDirectories.remove(NO_CONFIG);
if (none != null) {
if (none.size() == 1) {
out.printf("No 'lombok.config' found for '%s'.%n", none.iterator().next());
} else {
out.println("No 'lombok.config' found for: ");
for (String path : none) out.printf("- %s%n", path);
}
}
final List<String> problems = new ArrayList<String>();
ConfigurationProblemReporter reporter = new ConfigurationProblemReporter() {
@Override public void report(String sourceDescription, String problem, int lineNumber, CharSequence line) {
problems.add(String.format("%s: %s (%s:%d)", problem, line, sourceDescription, lineNumber));
}
};
FileSystemSourceCache cache = new FileSystemSourceCache();
boolean first = true;
for (Entry<URI, Set<String>> entry : sharedDirectories.entrySet()) {
if (!first) {
out.printf("%n%n");
}
Set<String> paths = entry.getValue();
if (paths.size() == 1) {
if (!(argsPaths.size() == 1)) out.printf("Configuration for '%s'.%n%n", paths.iterator().next());
} else {
out.printf("Configuration for:%n");
for (String path : paths) out.printf("- %s%n", path);
out.println();
}
URI directory = entry.getKey();
ConfigurationResolver resolver = new BubblingConfigurationResolver(cache.sourcesForDirectory(directory, reporter));
Map<ConfigurationKey<?>, ? extends Collection<String>> traces = trace(keys, directory);
boolean printed = false;
for (ConfigurationKey<?> key : keys) {
Object value = resolver.resolve(key);
Collection<String> modifications = traces.get(key);
if (!modifications.isEmpty() || explicitKeys) {
if (printed && verbose) out.println();
printValue(key, value, verbose, modifications);
printed = true;
}
}
if (!printed) out.println("<default>");
first = false;
}
if (!problems.isEmpty()) {
out.printf("%nProblems in the configuration files: %n");
for (String problem : problems) out.printf("- %s%n", problem);
}
return 0;
}
private void printValue(ConfigurationKey<?> key, Object value, boolean verbose, Collection<String> history) {
if (verbose) out.printf("# %s%n", key.getDescription());
if (value == null) {
out.printf("clear %s%n", key.getKeyName());
} else if (value instanceof List<?>) {
List<?> list = (List<?>)value;
if (list.isEmpty()) out.printf("clear %s%n", key.getKeyName());
for (Object element : list) out.printf("%s += %s%n", key.getKeyName(), element);
} else {
out.printf("%s = %s%n", key.getKeyName(), value);
}
if (!verbose) return;
for (String modification : history) out.printf("# %s%n", modification);
}
private static final ConfigurationProblemReporter VOID = new ConfigurationProblemReporter() {
@Override public void report(String sourceDescription, String problem, int lineNumber, CharSequence line) {}
};
private Map<ConfigurationKey<?>, ? extends Collection<String>> trace(Collection<ConfigurationKey<?>> keys, URI directory) throws Exception {
Map<ConfigurationKey<?>, List<String>> result = new HashMap<ConfigurationKey<?>, List<String>>();
for (ConfigurationKey<?> key : keys) result.put(key, new ArrayList<String>());
Set<ConfigurationKey<?>> used = new HashSet<ConfigurationKey<?>>();
boolean stopBubbling = false;
String previousFileName = null;
for (File currentDirectory = new File(directory); currentDirectory != null && !stopBubbling; currentDirectory = currentDirectory.getParentFile()) {
File configFile = new File(currentDirectory, "lombok.config");
if (!configFile.exists() || !configFile.isFile()) continue;
Map<ConfigurationKey<?>, List<String>> traces = trace(fileToString(configFile), configFile.getAbsolutePath(), keys);
stopBubbling = stopBubbling(traces.get(ConfigurationKeys.STOP_BUBBLING));
for (ConfigurationKey<?> key : keys) {
List<String> modifications = traces.get(key);
if (modifications == null) {
modifications = new ArrayList<String>();
modifications.add(" <'" + key.getKeyName() + "' not mentioned>");
} else {
used.add(key);
}
if (previousFileName != null) {
modifications.add("");
modifications.add(previousFileName + ":");
}
result.get(key).addAll(0, modifications);
}
previousFileName = configFile.getAbsolutePath();
}
for (ConfigurationKey<?> key : keys) {
if (used.contains(key)) {
result.get(key).add(0, previousFileName + (stopBubbling ? " (stopped bubbling):" : ":"));
} else {
result.put(key, Collections.<String>emptyList());
}
}
return result;
}
private Map<ConfigurationKey<?>, List<String>> trace(String content, String contentDescription, final Collection<ConfigurationKey<?>> keys) {
final Map<ConfigurationKey<?>, List<String>> result = new HashMap<ConfigurationKey<?>, List<String>>();
Collector collector = new Collector() {
@Override public void clear(ConfigurationKey<?> key, String contentDescription, int lineNumber) {
trace(key, "clear " + key.getKeyName(), lineNumber);
}
@Override public void set(ConfigurationKey<?> key, Object value, String contentDescription, int lineNumber) {
trace(key, key.getKeyName() + " = " + value, lineNumber);
}
@Override public void add(ConfigurationKey<?> key, Object value, String contentDescription, int lineNumber) {
trace(key, key.getKeyName() + " += " + value, lineNumber);
}
@Override public void remove(ConfigurationKey<?> key, Object value, String contentDescription, int lineNumber) {
trace(key, key.getKeyName() + " -= " + value, lineNumber);
}
private void trace(ConfigurationKey<?> key, String message, int lineNumber) {
if (!keys.contains(key)) return;
List<String> traces = result.get(key);
if (traces == null) {
traces = new ArrayList<String>();
result.put(key, traces);
}
traces.add(String.format("%4d: %s", lineNumber, message));
}
};
new ConfigurationParser(VOID).parse(content, contentDescription, collector);
return result;
}
private boolean stopBubbling(List<String> stops) {
return stops != null && !stops.isEmpty() && stops.get(stops.size() -1).endsWith("true");
}
private Collection<ConfigurationKey<?>> checkKeys(List<String> keyList) {
Map<String, ConfigurationKey<?>> registeredKeys = ConfigurationKey.registeredKeys();
if (keyList.isEmpty()) return registeredKeys.values();
Collection<ConfigurationKey<?>> keys = new ArrayList<ConfigurationKey<?>>();
for (String keyName : keyList) {
ConfigurationKey<?> key = registeredKeys.get(keyName);
if (key == null) {
err.printf("Unknown key '%s'%n", keyName);
return null;
}
keys.remove(key);
keys.add(key);
}
return keys;
}
private TreeMap<URI, Set<String>> findSharedDirectories(Collection<String> paths) {
TreeMap<URI,Set<String>> sharedDirectories = new TreeMap<URI, Set<String>>(new Comparator<URI>() {
@Override public int compare(URI o1, URI o2) {
return o1.toString().compareTo(o2.toString());
}
});
for (String path : paths) {
File file = new File(path);
if (!file.exists()) {
err.printf("File not found: '%s'%n", path);
return null;
}
URI first = findFirstLombokDirectory(file);
Set<String> sharedBy = sharedDirectories.get(first);
if (sharedBy == null) {
sharedBy = new TreeSet<String>();
sharedDirectories.put(first, sharedBy);
}
sharedBy.add(path);
}
return sharedDirectories;
}
private URI findFirstLombokDirectory(File file) {
File current = new File(file.toURI().normalize());
if (file.isFile()) current = current.getParentFile();
while (current != null) {
if (new File(current, "lombok.config").exists()) return current.toURI();
current = current.getParentFile();
}
return NO_CONFIG;
}
}