Class Reference content assist for boot application properties and yaml

This commit is contained in:
BoykoAlex
2016-11-30 13:14:27 -05:00
parent 6d68de4c8f
commit 5792f523fc
16 changed files with 331 additions and 54 deletions

View File

@@ -128,17 +128,17 @@ public abstract class CachingValueProvider implements ValueProviderStrategy {
}
}
// debug("full search for: "+query);
return getValuesAsycn(javaProject, query);
return getValuesAsync(javaProject, query);
}
protected abstract Flux<StsValueHint> getValuesAsycn(IJavaProject javaProject, String query);
protected abstract Flux<StsValueHint> getValuesAsync(IJavaProject javaProject, String query);
private Tuple2<String,String> key(IJavaProject javaProject, String query) {
return Tuples.of(javaProject==null?null:javaProject.getElementName(), query);
}
protected <K,V> Cache<K,V> createCache() {
return CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
return CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).expireAfterAccess(1, TimeUnit.MINUTES).build();
}
public static void restoreDefaults() {

View File

@@ -0,0 +1,135 @@
package org.springframework.ide.vscode.application.properties.metadata;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.application.properties.metadata.ValueProviderRegistry.ValueProviderStrategy;
import org.springframework.ide.vscode.application.properties.metadata.hints.StsValueHint;
import org.springframework.ide.vscode.commons.java.Flags;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import reactor.core.publisher.Flux;
public class ClassReferenceProvider extends CachingValueProvider {
/**
* Default value for the 'concrete' parameter.
*/
private static final boolean DEFAULT_CONCRETE = true;
private static final ClassReferenceProvider UNTARGETTED_INSTANCE = new ClassReferenceProvider(null, DEFAULT_CONCRETE);
public static final Function<Map<String, Object>, ValueProviderStrategy> FACTORY = applyOn(
1, TimeUnit.MINUTES,
(params) -> {
String target = getTarget(params);
Boolean concrete = getConcrete(params);
if (target!=null || concrete!=null) {
if (concrete==null) {
concrete = DEFAULT_CONCRETE;
}
return new ClassReferenceProvider(target, concrete);
}
return UNTARGETTED_INSTANCE;
}
);
public static <K,V> Function<K,V> applyOn(long duration, TimeUnit unit, Function<K,V> func) {
Cache<K,V> cache = CacheBuilder.newBuilder().expireAfterAccess(duration, unit).expireAfterWrite(duration, unit).build();
return (k) -> {
try {
return cache.get(k, () -> func.apply(k));
} catch (ExecutionException e) {
Log.log(e);
return null;
}
};
}
private static String getTarget(Map<String, Object> params) {
if (params!=null) {
Object obj = params.get("target");
if (obj instanceof String) {
String target = (String) obj;
if (StringUtil.hasText(target)) {
return target;
}
}
}
return null;
}
private static boolean isAbstract(IType type) {
try {
return type.isInterface() || Flags.isAbstract(type.getFlags());
} catch (Exception e) {
Log.log(e);
return false;
}
}
private static Boolean getConcrete(Map<String, Object> params) {
try {
if (params!=null) {
Object obj = params.get("concrete");
if (obj instanceof String) {
String concrete = (String) obj;
return Boolean.valueOf(concrete);
} else if (obj instanceof Boolean) {
return (Boolean) obj;
}
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
/**
* Optional, fully qualified name of the 'target' type. Suggested hints should be a subtype of this type.
*/
private String target;
/**
* Optional parameter, whether only concrete types should be suggested. Default value is true.
*/
private boolean concrete;
private ClassReferenceProvider(String target, boolean concrete) {
this.target = target;
this.concrete = concrete;
}
@Override
protected Flux<StsValueHint> getValuesAsync(IJavaProject javaProject, String query) {
IType targetType = target == null || target.isEmpty() ? javaProject.findType("java.lang.Object") : javaProject.findType(target);
if (targetType == null) {
return Flux.empty();
}
Set<IType> allSubclasses = javaProject
.allSubtypesOf(targetType)
.filter(t -> Flags.isPublic(t.getFlags()) && !concrete || !isAbstract(t))
.collect(Collectors.toSet())
.block();
if (allSubclasses.isEmpty()) {
return Flux.empty();
} else {
return javaProject
.fuzzySearchTypes(query, type -> allSubclasses.contains(type))
.collectSortedList((o1, o2) -> o2.getT2().compareTo(o1.getT2()))
.flatMap(l -> Flux.fromIterable(l))
.map(t -> StsValueHint.create(t.getT1()));
}
}
}

View File

@@ -51,7 +51,7 @@ public class ResourceHintProvider implements ValueProviderStrategy {
private static class ClasspathHints extends CachingValueProvider {
@Override
protected Flux<StsValueHint> getValuesAsycn(IJavaProject javaProject, String query) {
protected Flux<StsValueHint> getValuesAsync(IJavaProject javaProject, String query) {
return Flux.fromStream(javaProject.getClasspath().getClasspathResources().distinct().map(StsValueHint::create));
}
}

View File

@@ -49,7 +49,7 @@ public class ValueProviderRegistry {
protected void initializeDefaults(ValueProviderRegistry r) {
// def("logger-name", LoggerNameProvider.FACTORY);
// def("class-reference", ClassReferenceProvider.FACTORY);
def("class-reference", ClassReferenceProvider.FACTORY);
}
private Map<String, Function<Map<String, Object>, ValueProviderStrategy>> registry = new HashMap<>();

View File

@@ -51,5 +51,11 @@
<version>${roaster.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Reactor -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor-version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -4,11 +4,14 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -24,7 +27,9 @@ import org.springframework.ide.vscode.commons.java.IField;
import org.springframework.ide.vscode.commons.java.IJavadocProvider;
import org.springframework.ide.vscode.commons.java.IMethod;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.java.IJavaProject.TypeFilter;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.Log;
import com.google.common.base.Supplier;
@@ -32,17 +37,13 @@ import com.google.common.base.Suppliers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
public class JandexIndex {
private static class Entry<K, V> {
K key;
V value;
Entry(K key, V value) {
this.key = key;
this.value = value;
}
}
@FunctionalInterface
public static interface IndexFileFinder {
File findIndexFile(File jarFile);
@@ -77,10 +78,12 @@ public class JandexIndex {
};
private Supplier<List<Entry<File, IndexView>>> index;
private Map<File, Supplier<Optional<IndexView>>> index;
private JavadocProviderFactory javadocProviderFactory;
private Map<File, Supplier<List<Tuple2<String, IType>>>> knownTypes;
private Cache<File, IJavadocProvider> javadocProvidersCache = CacheBuilder.newBuilder().build();
private JandexIndex[] baseIndex;
@@ -93,26 +96,25 @@ public class JandexIndex {
return javadocProviderFactory;
}
public JandexIndex(Stream<Path> classpathEntries, IndexFileFinder indexFileFinder, JavadocProviderFactory javadocProviderFactory, JandexIndex... baseIndex) {
public JandexIndex(Collection<File> classpathEntries, IndexFileFinder indexFileFinder, JavadocProviderFactory javadocProviderFactory, JandexIndex... baseIndex) {
this.baseIndex = baseIndex;
index = Suppliers.memoize(() -> buildIndex(classpathEntries, indexFileFinder).collect(Collectors.toList()));
this.index = new ConcurrentHashMap<>();
this.knownTypes = new HashMap<>();
this.javadocProviderFactory = javadocProviderFactory;
classpathEntries.forEach(file -> {
index.put(file, Suppliers.memoize(() -> createIndex(file, indexFileFinder)));
knownTypes.put(file, Suppliers.memoize(() -> getKnownTypesStream(file).collect(Collectors.toList())));
});
}
private Stream<Entry<File, IndexView>> buildIndex(Stream<Path> classpathEntries, IndexFileFinder indexFileFinder) {
return classpathEntries
.map(entry -> entry.toFile())
.map(file -> {
Optional<IndexView> index = Optional.empty();
if (file.isFile() && file.getName().endsWith(".jar")) {
index = indexJar(file, indexFileFinder);
} else if (file.isDirectory()) {
index = indexFolder(file);
}
return new Entry<>(file, index);
})
.filter(e -> e.value.isPresent())
.map(e -> new Entry<>(e.key, e.value.get()));
private Optional<IndexView> createIndex(File file, IndexFileFinder indexFileFinder) {
if (file.isFile() && file.getName().endsWith(".jar")) {
return indexJar(file, indexFileFinder);
} else if (file.isDirectory()) {
return indexFolder(file);
} else {
return Optional.empty();
}
}
private static Optional<IndexView> indexFolder(File folder) {
@@ -181,7 +183,7 @@ public class JandexIndex {
public IType findType(String fqName) {
return getClassByName(DotName.createSimple(fqName));
}
IType getClassByName(DotName fqName) {
// First look for type in the base index array
return (baseIndex == null ? Stream.<IType>empty()
@@ -191,24 +193,71 @@ public class JandexIndex {
.filter(type -> type != null)
.findFirst()
// If not found look at indices owned by this JandexIndex instance
.orElseGet(() -> index.get().stream()
.map(e -> new Entry<>(e.key, e.value.getClassByName(fqName)))
.filter(e -> e.value != null)
.orElseGet(() -> streamOfIndices()
.map(e -> Tuples.of(e.getT1(), e.getT2().getClassByName(fqName)))
.filter(e -> e.getT2() != null)
.map(e -> createType(e))
.findFirst()
.orElse(null));
}
private IType createType(Entry<File, ClassInfo> match) {
File classpathResource = match.key;
private IType createType(Tuple2<File, ClassInfo> match) {
File classpathResource = match.getT1();
IJavadocProvider javadocProvider = null;
try {
javadocProvider = javadocProvidersCache.get(match.key, () -> javadocProviderFactory == null ? ABSENT_JAVADOC_PROVIDER : javadocProviderFactory.createJavadocProvider(classpathResource));
javadocProvider = javadocProvidersCache.get(classpathResource, () -> javadocProviderFactory == null ? ABSENT_JAVADOC_PROVIDER : javadocProviderFactory.createJavadocProvider(classpathResource));
} catch (ExecutionException e) {
Log.log(e);
}
return Wrappers.wrap(this, match.value, javadocProvider);
return Wrappers.wrap(this, match.getT2(), javadocProvider);
}
private Stream<Tuple2<File, IndexView>> streamOfIndices() {
return index.entrySet().parallelStream().map(e -> Tuples.of(e.getKey(), e.getValue().get())).filter(t -> t.getT2().isPresent()).map(t -> Tuples.of(t.getT1(), t.getT2().get()));
}
private Stream<Tuple2<String, IType>> getKnownTypesStream(File file) {
Optional<IndexView> indexView = index.get(file).get();
if (indexView.isPresent()) {
return indexView.get().getKnownClasses().parallelStream().map(info -> Tuples.of(info.name().toString(), createType(Tuples.of(file, info))));
}
return Stream.empty();
}
public Flux<Tuple2<IType, Double>> fuzzySearchTypes(String searchTerm, TypeFilter typeFilter) {
Flux<Tuple2<IType, Double>> flux = Flux.fromIterable(knownTypes.values())
.publishOn(Schedulers.parallel())
.flatMap(s -> Flux.fromIterable(s.get()))
.filter(t -> typeFilter == null || typeFilter.accept(t.getT2()))
.map(t -> Tuples.of(t.getT2(), FuzzyMatcher.matchScore(searchTerm, t.getT1())))
.filter(t -> t.getT2() != 0.0);
if (baseIndex == null) {
return flux;
} else {
return Flux.merge(flux, Flux.fromArray(baseIndex).flatMap(index -> index.fuzzySearchTypes(searchTerm, typeFilter)));
}
}
public Flux<IType> allSubtypesOf(IType type) {
DotName name = DotName.createSimple(type.getFullyQualifiedName());
Flux<IType> flux = Flux.fromIterable(index.keySet())
.publishOn(Schedulers.parallel())
.flatMap(file -> {
Optional<IndexView> optional = index.get(file).get();
if (optional.isPresent()) {
return Flux.fromIterable(type.isInterface() ? optional.get().getAllKnownImplementors(name) : optional.get().getAllKnownSubclasses(name))
.publishOn(Schedulers.parallel())
.map(info -> createType(Tuples.of(file, info)));
} else {
return Flux.empty();
}
});
if (baseIndex == null) {
return flux;
} else {
return Flux.merge(flux, Flux.fromArray(baseIndex).flatMap(index -> index.allSubtypesOf(type)));
}
}
}

View File

@@ -4,6 +4,7 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;
@@ -57,7 +58,8 @@ class TypeImpl implements IType {
@Override
public Stream<IAnnotation> getAnnotations() {
// TODO: check correctness!
return info.annotations().get(info.name()).stream().map(a -> Wrappers.wrap(a, javadocProvider));
List<AnnotationInstance> annotations = info.annotations().get(info.name());
return annotations == null ? Stream.empty() : annotations.stream().map(a -> Wrappers.wrap(a, javadocProvider));
}
@Override
@@ -117,13 +119,13 @@ class TypeImpl implements IType {
@Override
public int hashCode() {
return info.toString().hashCode();
return info.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof TypeImpl) {
return info.toString().equals(((TypeImpl)obj).info.toString());
return info.equals(((TypeImpl)obj).info);
}
return super.equals(obj);
}

View File

@@ -1,6 +1,20 @@
package org.springframework.ide.vscode.commons.java;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
public interface IJavaProject extends IJavaElement {
@FunctionalInterface
public static interface TypeFilter {
boolean accept(IType type);
}
IType findType(String fqName);
Flux<Tuple2<IType, Double>> fuzzySearchTypes(String searchTerm, TypeFilter typeFilter);
Flux<IType> allSubtypesOf(IType type);
IClasspath getClasspath();
}

View File

@@ -84,7 +84,7 @@ public class MavenCore {
private Supplier<JandexIndex> javaCoreIndex = Suppliers.memoize(() -> {
try {
return new JandexIndex(getJreLibs(), jarFile -> findIndexFile(jarFile), (classpathResource) -> {
return new JandexIndex(getJreLibs().map(path -> path.toFile()).collect(Collectors.toList()), jarFile -> findIndexFile(jarFile), (classpathResource) -> {
try {
String javaVersion = getJavaRuntimeMinorVersion();
if (javaVersion == null) {
@@ -283,7 +283,14 @@ public class MavenCore {
private File findIndexFile(File jarFile) {
String suffix = null;
try {
if (jarFile.toString().startsWith(getJavaHome())) {
String javaHome = getJavaHome();
if (javaHome != null) {
int index = javaHome.lastIndexOf('/');
if (index != -1) {
javaHome = javaHome.substring(0, index);
}
}
if (jarFile.toString().startsWith(javaHome)) {
suffix = getJavaRuntimeVersion();
}
} catch (MavenException e) {

View File

@@ -15,12 +15,14 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.maven.project.MavenProject;
import org.springframework.ide.vscode.commons.java.IClasspath;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
import org.springframework.ide.vscode.commons.maven.MavenCore;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
/**
* Wrapper for Maven Core project
*
@@ -60,12 +62,22 @@ public class MavenJavaProject implements IJavaProject {
}
@Override
public IClasspath getClasspath() {
public Flux<Tuple2<IType, Double>> fuzzySearchTypes(String searchTerm, TypeFilter typeFilter) {
return classpath.fuzzySearchType(searchTerm, typeFilter);
}
@Override
public Flux<IType> allSubtypesOf(IType type) {
return classpath.allSubtypesOf(type);
}
@Override
public MavenProjectClasspath getClasspath() {
return classpath;
}
public Path getOutputFolder() {
return Paths.get(new File(mavenProject.getBuild().getOutputDirectory()).toURI());
}
}

View File

@@ -16,6 +16,7 @@ import java.net.URL;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.artifact.Artifact;
@@ -25,6 +26,7 @@ import org.springframework.ide.vscode.commons.jandex.JandexIndex;
import org.springframework.ide.vscode.commons.java.IClasspath;
import org.springframework.ide.vscode.commons.java.IJavadocProvider;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.java.IJavaProject.TypeFilter;
import org.springframework.ide.vscode.commons.java.parser.ParserJavadocProvider;
import org.springframework.ide.vscode.commons.java.roaster.RoasterJavadocProvider;
import org.springframework.ide.vscode.commons.javadoc.HtmlJavadocProvider;
@@ -36,6 +38,9 @@ import org.springframework.ide.vscode.commons.util.Log;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
/**
* Classpath for a maven project
*
@@ -70,7 +75,7 @@ public class MavenProjectClasspath implements IClasspath {
} catch (Exception e) {
Log.log(e);
}
return new JandexIndex(classpathEntries, jarFile -> findIndexFile(jarFile), classpathResource -> {
return new JandexIndex(classpathEntries.map(p -> p.toFile()).collect(Collectors.toList()), jarFile -> findIndexFile(jarFile), classpathResource -> {
switch (providerType) {
case JAVA_PARSER:
return createParserJavadocProvider(classpathResource);
@@ -95,6 +100,14 @@ public class MavenProjectClasspath implements IClasspath {
return javaIndex.get().findType(fqName);
}
public Flux<Tuple2<IType, Double>> fuzzySearchType(String searchTerm, TypeFilter typeFilter) {
return javaIndex.get().fuzzySearchTypes(searchTerm, typeFilter);
}
public Flux<IType> allSubtypesOf(IType type) {
return javaIndex.get().allSubtypesOf(type);
}
private File findIndexFile(File jarFile) {
return new File(maven.getIndexFolder().toString(), jarFile.getName() + "-" + jarFile.lastModified() + ".jdx");
}
@@ -219,5 +232,4 @@ public class MavenProjectClasspath implements IClasspath {
}
}
}

View File

@@ -18,6 +18,9 @@ import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
/**
* Java project that contains classpath text file
*
@@ -55,6 +58,16 @@ public class JavaProjectWithClasspathFile implements IJavaProject {
return null;
}
@Override
public Flux<Tuple2<IType, Double>> fuzzySearchTypes(String searchTerm, TypeFilter typeFilter) {
return Flux.empty();
}
@Override
public Flux<IType> allSubtypesOf(IType type) {
return Flux.empty();
}
@Override
public IClasspath getClasspath() {
return classpath;
@@ -90,5 +103,4 @@ public class JavaProjectWithClasspathFile implements IJavaProject {
return true;
}
}

View File

@@ -3,15 +3,18 @@ package org.springframework.ide.vscode.commons.maven;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Assume;
import org.junit.Test;
import org.springframework.ide.vscode.commons.java.Flags;
import org.springframework.ide.vscode.commons.java.IField;
import org.springframework.ide.vscode.commons.java.IMethod;
import org.springframework.ide.vscode.commons.java.IPrimitiveType;
@@ -26,6 +29,8 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import reactor.util.function.Tuple2;
public class JavaIndexTest {
private static LoadingCache<String, Path> projectsCache = CacheBuilder.newBuilder().build(new CacheLoader<String, Path>() {
@@ -62,6 +67,26 @@ public class JavaIndexTest {
}
}
@Test
public void fuzzySearchNoFilter() throws Exception {
List<Tuple2<IType, Double>> results = MavenCore.getInstance().getJavaIndexForJreLibs()
.fuzzySearchTypes("util.Map", null)
.collectSortedList((o1, o2) -> o2.getT2().compareTo(o1.getT2()))
.block();
assertTrue(results.size() > 10);
assertEquals("java.util.Map", results.get(0).getT1().getFullyQualifiedName());
}
@Test
public void fuzzySearchWithFilter() throws Exception {
List<Tuple2<IType, Double>> results = MavenCore.getInstance().getJavaIndexForJreLibs()
.fuzzySearchTypes("util.Map", (type) -> Flags.isPrivate(type.getFlags()))
.collectSortedList((o1, o2) -> o2.getT2().compareTo(o1.getT2()))
.block();
assertTrue(results.size() > 10);
assertEquals("java.util.EnumMap$KeySet", results.get(0).getT1().getFullyQualifiedName());
}
@Test
public void findClassInJar() throws Exception {
MavenJavaProject project = mavenProjectsCache.get("gs-rest-service-cors-boot-1.4.1-with-classpath-file");

View File

@@ -0,0 +1,3 @@
**/classpath.txt
**/bin/**
**/*.log.*

View File

@@ -1331,7 +1331,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
}
}
@Ignore @Test public void testClassReferenceCompletion() throws Exception {
@Test public void testClassReferenceCompletion() throws Exception {
CachingValueProvider.TIMEOUT = Duration.ofSeconds(20);
useProject(createPredefinedMavenProject("empty-boot-1.3.0-with-mongo"));

View File

@@ -3247,7 +3247,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest {
);
}
@Ignore @Test public void testClassReferenceCompletion() throws Exception {
@Test public void testClassReferenceCompletion() throws Exception {
CachingValueProvider.TIMEOUT = Duration.ofSeconds(20);
useProject(createPredefinedMavenProject("empty-boot-1.3.0-with-mongo"));