Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #4628 - Non-Required Module Dependency Support #4629

Merged
merged 1 commit into from
Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions jetty-start/src/main/java/org/eclipse/jetty/start/Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -154,12 +153,12 @@ public class Module implements Comparable<Module>
private final List<String> _license = new ArrayList<>();

/**
* Dependencies
* Dependencies from {@code [depends]} section
*/
private final List<String> _depends = new ArrayList<>();

/**
* Optional
* Optional dependencies from {@code [optional]} section are structural in nature.
*/
private final Set<String> _optional = new HashSet<>();

Expand Down Expand Up @@ -196,6 +195,18 @@ public Module(BaseHome basehome, Path path) throws IOException
process(basehome);
}

public static boolean isRequiredDependency(String depends)
{
return (depends != null) && (depends.charAt(0) != '?');
}

public static String normalizeModuleName(String name)
{
if (!isRequiredDependency(name))
return name.substring(1);
return name;
}

public String getName()
{
return _name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ private void writeRelationships(PrintWriter out, Iterable<Module> modules, List<
{
for (String depends : module.getDepends())
{
depends = Module.normalizeModuleName(depends);
out.printf(" \"%s\" -> \"%s\";%n", module.getName(), depends);
}
for (String optional : module.getOptional())
Expand Down
52 changes: 36 additions & 16 deletions jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -132,7 +133,12 @@ public void dump(List<String> tags)
label = " Depend: %s";
for (String parent : module.getDepends())
{
parent = Module.normalizeModuleName(parent);
System.out.printf(label, parent);
if (!Module.isRequiredDependency(parent))
{
System.out.print(" [not-required]");
}
label = ", %s";
}
System.out.println();
Expand Down Expand Up @@ -352,45 +358,59 @@ private void enable(Set<String> newlyEnabled, Module module, String enabledFrom,

// Process module dependencies (always processed as may be dynamic)
StartLog.debug("Enabled module %s depends on %s", module.getName(), module.getDepends());
for (String dependsOn : module.getDepends())
for (String dependsOnRaw : module.getDepends())
{
boolean isRequired = Module.isRequiredDependency(dependsOnRaw);
// Final to allow lambda's below to use name
final String dependentModule = Module.normalizeModuleName(dependsOnRaw);

// Look for modules that provide that dependency
Set<Module> providers = getAvailableProviders(dependsOn);
Set<Module> providers = getAvailableProviders(dependentModule);

StartLog.debug("Module %s depends on %s provided by %s", module, dependsOn, providers);
StartLog.debug("Module %s depends on %s provided by %s", module, dependentModule, providers);

// If there are no known providers of the module
if (providers.isEmpty())
{
// look for a dynamic module
if (dependsOn.contains("/"))
if (dependentModule.contains("/"))
{
Path file = _baseHome.getPath("modules/" + dependsOn + ".mod");
registerModule(file).expandDependencies(_args.getProperties());
providers = _provided.get(dependsOn);
if (providers == null || providers.isEmpty())
throw new UsageException("Module %s does not provide %s", _baseHome.toShortForm(file), dependsOn);
Path file = _baseHome.getPath("modules/" + dependentModule + ".mod");
if (isRequired || Files.exists(file))
{
registerModule(file).expandDependencies(_args.getProperties());
providers = _provided.get(dependentModule);
if (providers == null || providers.isEmpty())
throw new UsageException("Module %s does not provide %s", _baseHome.toShortForm(file), dependentModule);

enable(newlyEnabled, providers.stream().findFirst().get(), "dynamic dependency of " + module.getName(), true);
enable(newlyEnabled, providers.stream().findFirst().get(), "dynamic dependency of " + module.getName(), true);
continue;
}
}
// is this a non-required module
if (!isRequired)
{
StartLog.debug("Skipping non-required module [%s]: doesn't exist", dependentModule);
continue;
}
throw new UsageException("No module found to provide %s for %s", dependsOn, module);
// throw an exception (not a dynamic module and a required dependency)
throw new UsageException("No module found to provide %s for %s", dependentModule, module);
}

// If a provider is already enabled, then add a transitive enable
if (providers.stream().filter(Module::isEnabled).count() > 0)
providers.stream().filter(m -> m.isEnabled() && !m.equals(module)).forEach(m -> enable(newlyEnabled, m, "transitive provider of " + dependsOn + " for " + module.getName(), true));
if (providers.stream().anyMatch(Module::isEnabled))
providers.stream().filter(m -> m.isEnabled() && !m.equals(module)).forEach(m -> enable(newlyEnabled, m, "transitive provider of " + dependentModule + " for " + module.getName(), true));
else
{
// Is there an obvious default?
Optional<Module> dftProvider = (providers.size() == 1)
? providers.stream().findFirst()
: providers.stream().filter(m -> m.getName().equals(dependsOn)).findFirst();
: providers.stream().filter(m -> m.getName().equals(dependentModule)).findFirst();

if (dftProvider.isPresent())
enable(newlyEnabled, dftProvider.get(), "transitive provider of " + dependsOn + " for " + module.getName(), true);
enable(newlyEnabled, dftProvider.get(), "transitive provider of " + dependentModule + " for " + module.getName(), true);
else if (StartLog.isDebugEnabled())
StartLog.debug("Module %s requires a %s implementation from one of %s", module, dependsOn, providers);
StartLog.debug("Module %s requires a %s implementation from one of %s", module, dependentModule, providers);
}
}
}
Expand Down
148 changes: 148 additions & 0 deletions jetty-start/src/test/java/org/eclipse/jetty/start/ModulesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;

@ExtendWith(WorkDirExtension.class)
public class ModulesTest
Expand Down Expand Up @@ -202,6 +204,152 @@ public void testResolveServerHttp() throws IOException
assertThat("Resolved XMLs: " + actualXmls, actualXmls, contains(expectedXmls.toArray()));
}

@Test
public void testResolveNotRequiredModuleNotFound() throws IOException
{
// Test Env
File homeDir = MavenTestingUtils.getTestResourceDir("non-required-deps");
File baseDir = testdir.getEmptyPathDir().toFile();
String[] cmdLine = new String[]{"bar.type=cannot-find-me"};

// Configuration
CommandLineConfigSource cmdLineSource = new CommandLineConfigSource(cmdLine);
ConfigSources config = new ConfigSources();
config.add(cmdLineSource);
config.add(new JettyHomeConfigSource(homeDir.toPath()));
config.add(new JettyBaseConfigSource(baseDir.toPath()));

// Initialize
BaseHome basehome = new BaseHome(config);

StartArgs args = new StartArgs(basehome);
args.parse(config);

// Test Modules
Modules modules = new Modules(basehome, args);
modules.registerAll();

// Enable module
modules.enable("bar", TEST_SOURCE);

// Collect active module list
List<Module> active = modules.getEnabled();

// Assert names are correct, and in the right order
List<String> expectedNames = new ArrayList<>();
expectedNames.add("foo");
expectedNames.add("bar");

List<String> actualNames = new ArrayList<>();
for (Module actual : active)
{
actualNames.add(actual.getName());
}

assertThat("Resolved Names: " + actualNames, actualNames, contains(expectedNames.toArray()));

Props props = args.getProperties();
assertThat(props.getString("bar.name"), is(nullValue()));
}

@Test
public void testResolveNotRequiredModuleFound() throws IOException
{
// Test Env
File homeDir = MavenTestingUtils.getTestResourceDir("non-required-deps");
File baseDir = testdir.getEmptyPathDir().toFile();
String[] cmdLine = new String[]{"bar.type=dive"};

// Configuration
CommandLineConfigSource cmdLineSource = new CommandLineConfigSource(cmdLine);
ConfigSources config = new ConfigSources();
config.add(cmdLineSource);
config.add(new JettyHomeConfigSource(homeDir.toPath()));
config.add(new JettyBaseConfigSource(baseDir.toPath()));

// Initialize
BaseHome basehome = new BaseHome(config);

StartArgs args = new StartArgs(basehome);
args.parse(config);

// Test Modules
Modules modules = new Modules(basehome, args);
modules.registerAll();

// Enable module
modules.enable("bar", TEST_SOURCE);

// Collect active module list
List<Module> active = modules.getEnabled();

// Assert names are correct, and in the right order
List<String> expectedNames = new ArrayList<>();
expectedNames.add("foo");
expectedNames.add("bar");
expectedNames.add("bar-dive");

List<String> actualNames = new ArrayList<>();
for (Module actual : active)
{
actualNames.add(actual.getName());
}

assertThat("Resolved Names: " + actualNames, actualNames, contains(expectedNames.toArray()));

Props props = args.getProperties();
assertThat(props.getString("bar.name"), is("dive"));
}

@Test
public void testResolveNotRequiredModuleFoundDynamic() throws IOException
{
// Test Env
File homeDir = MavenTestingUtils.getTestResourceDir("non-required-deps");
File baseDir = testdir.getEmptyPathDir().toFile();
String[] cmdLine = new String[]{"bar.type=dynamic"};

// Configuration
CommandLineConfigSource cmdLineSource = new CommandLineConfigSource(cmdLine);
ConfigSources config = new ConfigSources();
config.add(cmdLineSource);
config.add(new JettyHomeConfigSource(homeDir.toPath()));
config.add(new JettyBaseConfigSource(baseDir.toPath()));

// Initialize
BaseHome basehome = new BaseHome(config);

StartArgs args = new StartArgs(basehome);
args.parse(config);

// Test Modules
Modules modules = new Modules(basehome, args);
modules.registerAll();

// Enable module
modules.enable("bar", TEST_SOURCE);

// Collect active module list
List<Module> active = modules.getEnabled();

// Assert names are correct, and in the right order
List<String> expectedNames = new ArrayList<>();
expectedNames.add("foo");
expectedNames.add("bar");
expectedNames.add("impls/bar-dynamic");

List<String> actualNames = new ArrayList<>();
for (Module actual : active)
{
actualNames.add(actual.getName());
}

assertThat("Resolved Names: " + actualNames, actualNames, contains(expectedNames.toArray()));

Props props = args.getProperties();
assertThat(props.getString("bar.name"), is("dynamic"));
}

private List<String> normalizeLibs(List<Module> active)
{
List<String> libs = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[ini]
bar.name=dive
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Top level mod

[depends]
foo
?bar-${bar.type}
?impls/bar-${bar.type}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# nothing here
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[ini]
bar.name=dynamic