/*
* Copyright 2013, We The Internet Ltd.
*
* All rights reserved.
*
* Distributed under a modified BSD License as follow:
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistribution in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution, unless otherwise
* agreed to in a written document signed by a director of We The Internet Ltd.
*
* Neither the name of We The Internet nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package xapi.dev.source;
import xapi.fu.In1;
import static xapi.dev.source.PrintBuffer.NEW_LINE;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class ImportSection implements CanAddImports {
public static class PackageAwareImports extends ImportSection implements HasPackage {
private String packageName;
@Override
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
}
// It's ok to use a plain hashmap, we sort all imports before printing.
private final Map<String, String> imports = new HashMap<>();
private final Map<String, String> importStatic = new HashMap<>();
private final Set<String> starImports = new HashSet<>();
private boolean canIgnoreOwnPackage;
private boolean replaceDollarSign;
public ImportSection() {
canIgnoreOwnPackage = true;
}
public boolean hasStarImports() {
return !starImports.isEmpty();
}
public ImportSection addImports(final String... imports) {
for (final String iport : imports) {
addImport(iport);
}
return this;
}
public String addImport(final String importName) {
return tryImport(importName, importName.contains("static "), false);
}
public String addImport(final String importName, boolean skipNoPackages) {
return tryImport(importName, importName.contains("static "), skipNoPackages);
}
public String addStaticImport(final Class<?> cls, final String importName) {
return addStaticImport(cls.getCanonicalName(), importName);
}
public String addStaticImport(final String cls, final String importName) {
final boolean hasStatic = importName!=null&&importName.length()>0;
return tryImport(cls+(hasStatic ? "."+importName : ""), hasStatic, shouldReplaceDollarSign());
}
public boolean shouldReplaceDollarSign() {
return replaceDollarSign;
}
public String addStaticImport(final String importName) {
return tryImport(importName, true, false);
}
public ImportSection addStatics(final String... imports) {
for (final String iport : imports) {
addStaticImport(iport);
}
return this;
}
@Override
public final String toString() {
return toSource();
}
public String toSource() {
final StringBuilder b = new StringBuilder();
// Print static imports first
String[] values = importStatic.values().toArray(new String[importStatic.size()]);
// nicely sorted
Arrays.sort(values);
printImports(b, values, "import static ");
if (values.length > 0) {
b.append(NEW_LINE);
}
values = imports.values().toArray(new String[imports.size()]);
Arrays.sort(values);
printImports(b, values, "import ");
return b.toString();
}
private void printImports(final StringBuilder b, final String[] values, final String importType) {
if (values.length == 0) {
return;
}
String prefix = prefixOf(values[0]);
for (final String importName : values) {
if (importName.length() > 0) {
final String newPrefix = prefixOf(importName);
if (!newPrefix.equals(prefix)) {
b.append(NEW_LINE);
prefix = newPrefix;
}
b.append(importType).append(importName).append(';').append(NEW_LINE);
}
}
}
/**
* We keep track of the prefix of each import,
* so we can group similar packages together.
*
* This uses the first packagename,
* so all com.* and org.* will be grouped together,
* but for concise packagenames like xapi.* or elemental.*,
* this will ensure we don't get a spamming of newlines.
*
*/
private String prefixOf(final String string) {
final int ind = string.indexOf('.');
return ind == -1 ? string : string.substring(0, ind);
}
public ImportSection reserveSimpleName(final String cls) {
if (!imports.containsKey(cls)) {
imports.put(cls, "");
}
return this;
}
public String addImport(final Class<?> cls) {
if (cls.isPrimitive() || "java.lang".equals(cls.getPackage().getName())) {
return cls.getSimpleName();
}
final String existing = imports.get(cls.getSimpleName());
if (existing != null) {
if (existing.equals(cls.getCanonicalName())) {
return cls.getSimpleName();
}
return cls.getCanonicalName();
}
imports.put(cls.getSimpleName(), cls.getCanonicalName());
return cls.getSimpleName();
}
public ImportSection addImports(final Class<?>... imports) {
for (final Class<?> cls : imports) {
addImport(cls);
}
return this;
}
protected boolean canMinimize(final String importName) {
final String simpleName = importName.substring(importName.lastIndexOf('.') + 1);
final String existing = imports.get(simpleName);
return existing == null || // No type for this simple name
"".equals(existing) || // This simple name was reserved
existing.equals(importName); // This type is already imported
}
protected String tryImport(final String importName, final boolean staticImport, boolean skipNoPackage) {
return tryImport(importName, staticImport, shouldReplaceDollarSign(), skipNoPackage);
}
protected String tryImport(String importName, final boolean staticImport, final boolean replaceDollarSigns, final boolean skipNoPackages) {
final String originalImport = importName;
final Map<String, String> map = staticImport ? importStatic : imports;
int arrayDepth = 0;
int index = importName.indexOf(".");
// do not import primitives
final boolean hasDot = index != -1;
importName = trim(importName);
// rip off generics, and optionally try to import them as well
// Be sure to do generics before array types, or else List<String[]> will become List<String>[]
index = importName.indexOf('<');
String suffix;
if (index == -1) {
suffix = "";
} else {
suffix = importName.substring(index);
suffix = importFullyQualifiedNames(suffix);
importName = importName.substring(0, index);
}
// ignore any []
index = importName.indexOf("[]");
while (index != -1) {
importName = importName.substring(0, index)
+ (index < importName.length() - 2 ? importName.substring(index + 2)
: "");
index = importName.indexOf("[]", index);
arrayDepth++;
}
// put back any array definitions we ignored
while (arrayDepth-- > 0) {
suffix += "[]";
}
// check if we need to import this type...
if (skipImports(importName, skipNoPackages)) {
if (importName.startsWith("java.lang.")) {
importName = importName.substring(10);
}
return importName + suffix;
}
if (hasDot){
// a name with a . in it; check if we need to import it
if (!staticImport && canIgnoreOwnPackage() && this instanceof HasPackage) {
String pkg = ((HasPackage)this).getPackageName();
if (pkg != null && !pkg.isEmpty()) {
final String noPkg = importName.replace(pkg + ".", "");
if (noPkg.indexOf('.') == -1) {
String existing = map.get(noPkg);
if (existing == null || existing.isEmpty()) {
map.put(noPkg, importName);
return noPkg + suffix;
} else {
if (importName.equals(existing)) {
return noPkg + suffix;
} else {
return existing + suffix;
}
}
}
}
}
}
if (!staticImport && skipImports(importName, skipNoPackages)) {
return importName.replace("java.lang.", "") + suffix;
}
if (replaceDollarSigns) {
importName = importName.replace('$', '.');
}
final String shortname = importName.substring(1 + importName.lastIndexOf('.'));
if ("*".equals(shortname)) {
map.put(importName, importName);
assert suffix.length() == 0 : "Bad import; has a suffix with a * import: " + originalImport;
return importName;
}
final String existing = map.get(shortname);
if (existing == null || existing.isEmpty()) {
map.put(shortname, importName);
return shortname + suffix;
}
// if the existing match was this classname, we are allowed to return shortname
if (existing.equals(importName)) {
return shortname + suffix;
}
// if there was an existing name that wasn't us, we can't perform the import.
return importName + suffix;
}
public String importFullyQualifiedNames(String suffix) {
// use two builders: one for the final result,
StringBuilder result = new StringBuilder();
// and the other for java packagenames (java identifiers and dots)
StringBuilder word = new StringBuilder();
// we'll want to use the logic to clear the word buffer more than once...
In1<Boolean> tryImport = hasDot->{
if (word.length() > 0) {
if (hasDot) {
String imported = addImport(word.toString(), true);
result.append(imported);
} else {
result.append(word);
}
word.setLength(0);
}
};
// loop through whole string
boolean hasDot = false;
int pos = 0;
while (pos < suffix.length()) {
char c = suffix.charAt(pos++);
// record . and java identifiers in word builders
if (c == '.') {
hasDot = true;
word.append(c);
} else {
if (Character.isJavaIdentifierPart(c)) {
word.append(c);
} else {
tryImport.in(hasDot);
hasDot = false;
result.append(c);
}
}
}
tryImport.in(hasDot);
return result.toString();
}
protected boolean canIgnoreOwnPackage() {
return canIgnoreOwnPackage;
}
private boolean skipImports(final String importName, boolean skipSingleNames) {
if (importName.matches("("
+ "(java[.]lang.[^.]*)" + // discard java.lang, but keep java.lang.reflect
"|" + // also discard primitives
"(void)|(boolean)|(short)|(char)|(int)|(long)|(float)|(double)"
+ "|extends|super|import|static|[?]"
+ "|(String)|(Class)|(Object)|(Void)|(Boolean)|(Short)|(Character)|(Integer)|(Long)|(Float)|(Double)|(Iterable))"
+ "[;]*")) {
return true;
}
return skipSingleNames && importName.indexOf('.') == -1;
}
private String trim(final String importName) {
return importName.replaceAll(
//"(\\[\\s*\\])|" +
"(\\s*import\\s+)|" +
"(\\s*static\\s+)|" +
"(\\s*;\\s*)", "");
}
@Override
public ImportSection getImports() {
return this;
}
public void setCanIgnoreOwnPackage(boolean canIgnoreOwnPackage) {
this.canIgnoreOwnPackage = canIgnoreOwnPackage;
}
public void setReplaceDollarSign(boolean replaceDollarSign) {
this.replaceDollarSign = replaceDollarSign;
}
}