//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.webapp;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.TopologicalSort;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* An ordered list of {@link Configuration} instances.
* <p>
* The ordering of Configurations will initially be the order in which they
* are added. The {@link #sort()} method can be used to apply a
* {@link TopologicalSort} to the ordering as defined by the
* {@link Configuration#getDependencies()} and
* {@link Configuration#getDependents()} methods.
* Instances that do not have ordering dependencies will maintain
* their add order, as will additions/insertions made after the
* the sort.
* </p>
* <p>
* If an added {@link Configuration} returns a value for
* {@link Configuration#replaces()} then the added instance will replace
* any existing instance of that type or that has already replaced that
* type.
* </p>
*/
public class Configurations extends AbstractList<Configuration>
{
private static final Logger LOG = Log.getLogger(Configurations.class);
private static final List<Configuration> __known = new ArrayList<>();
private static final Set<String> __knownByClassName = new HashSet<>();
/* ------------------------------------------------------------ */
public static synchronized List<Configuration> getKnown()
{
if (__known.isEmpty())
{
ServiceLoader<Configuration> configs = ServiceLoader.load(Configuration.class);
for (Configuration configuration : configs)
{
__known.add(configuration);
__knownByClassName.add(configuration.getClass().getName());
}
sort(__known);
if (LOG.isDebugEnabled())
{
for (Configuration c: __known)
LOG.debug("known {}",c);
}
LOG.debug("Known Configurations {}",__knownByClassName);
}
return __known;
}
/* ------------------------------------------------------------ */
public static synchronized void setKnown (String ... classes)
{
if (!__known.isEmpty())
throw new IllegalStateException("Known configuration classes already set");
for (String c:classes)
{
try
{
Class<?> clazz = Loader.loadClass(c);
__known.add((Configuration)clazz.newInstance());
__knownByClassName.add(c);
}
catch (Exception e)
{
LOG.warn("Problem loading known class",e);
}
}
sort(__known);
if (LOG.isDebugEnabled())
{
for (Configuration c: __known)
LOG.debug("known {}",c);
}
LOG.debug("Known Configurations {}",__knownByClassName);
}
/* ------------------------------------------------------------ */
static synchronized void cleanKnown()
{
__known.clear();
}
/* ------------------------------------------------------------ */
/** Get/Set/Create the server default Configuration ClassList.
* <p>Get the class list from: a Server bean; or the attribute (which can
* either be a ClassList instance or an String[] of class names); or a new instance
* with default configuration classes.</p>
* <p>This method also adds the obtained ClassList instance as a dependent bean
* on the server and clears the attribute</p>
* @param server The server the default is for
* @return the server default ClassList instance of the configuration classes for this server.
* Changes to this list will change the server default instance.
*/
public static Configurations setServerDefault(Server server)
{
Configurations configurations=server.getBean(Configurations.class);
if (configurations!=null)
return configurations;
configurations=getServerDefault(server);
server.addBean(configurations);
server.setAttribute(Configuration.ATTR,null);
return configurations;
}
/* ------------------------------------------------------------ */
@Deprecated
public static Configurations serverDefault(Server server)
{
return getServerDefault(server);
}
/* ------------------------------------------------------------ */
/** Get/Create the server default Configuration ClassList.
* <p>Get the class list from: a Server bean; or the attribute (which can
* either be a ClassList instance or an String[] of class names); or a new instance
* with default configuration classes.
* @param server The server the default is for
* @return A copy of the server default ClassList instance of the configuration classes for this server.
* Changes to the returned list will not change the server default.
*/
public static Configurations getServerDefault(Server server)
{
Configurations configurations=null;
if (server!=null)
{
configurations= server.getBean(Configurations.class);
if (configurations!=null)
configurations= new Configurations(configurations);
else
{
Object attr = server.getAttribute(Configuration.ATTR);
LOG.debug("{} attr({})= {}",server,Configuration.ATTR,attr);
if (attr instanceof Configurations)
configurations = new Configurations((Configurations)attr);
else if (attr instanceof String[])
configurations = new Configurations((String[])attr);
}
}
if (configurations==null)
{
configurations=new Configurations(Configurations.getKnown().stream()
.filter(c->!c.isDisabledByDefault())
.map(c->c.getClass().getName())
.toArray(String[]::new));
}
if (LOG.isDebugEnabled())
LOG.debug("default configurations for {}: {}",server,configurations);
return configurations;
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
protected List<Configuration> _configurations = new ArrayList<>();
public Configurations()
{
}
protected static Configuration newConfiguration(String classname)
{
if (LOG.isDebugEnabled())
{
if (!__knownByClassName.contains(classname))
LOG.warn("Unknown configuration {}. Not declared for ServiceLoader!",classname);
}
try
{
@SuppressWarnings("unchecked")
Class<Configuration> clazz = Loader.loadClass(classname);
return clazz.newInstance();
}
catch (ClassNotFoundException | InstantiationException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
public Configurations(String... classes)
{
add(classes);
}
public Configurations(List<String> classes)
{
add(classes.toArray(new String[classes.size()]));
}
public Configurations(Configurations classlist)
{
this(classlist._configurations.stream()
.map(c->c.getClass().getName())
.toArray(String[]::new));
}
public void add(Configuration... configurations)
{
for (Configuration configuration : configurations)
addConfiguration(configuration);
}
public void add(@Name("configClass")String... configClass)
{
for (String name : configClass)
addConfiguration(newConfiguration(name));
}
public void clear()
{
_configurations.clear();
}
public void set(Configuration... configurations)
{
clear();
add(configurations);
}
public void set(@Name("configClass")String... configClass)
{
clear();
add(configClass);
}
public void remove(Configuration... configurations)
{
List<String> names = Arrays.asList(configurations).stream().map(c->c.getClass().getName()).collect(Collectors.toList());
for (ListIterator<Configuration> i=_configurations.listIterator();i.hasNext();)
{
Configuration configuration=i.next();
if (names.contains(configuration.getClass().getName()))
i.remove();
}
}
public void remove(Class<? extends Configuration>... configClass)
{
List<String> names = Arrays.asList(configClass).stream().map(c->c.getName()).collect(Collectors.toList());
for (ListIterator<Configuration> i=_configurations.listIterator();i.hasNext();)
{
Configuration configuration=i.next();
if (names.contains(configuration.getClass().getName()))
i.remove();
}
}
public void remove(@Name("configClass")String... configClass)
{
List<String> names = Arrays.asList(configClass);
for (ListIterator<Configuration> i=_configurations.listIterator();i.hasNext();)
{
Configuration configuration=i.next();
if (names.contains(configuration.getClass().getName()))
i.remove();
}
}
public int size()
{
return _configurations.size();
}
public String[] toArray()
{
return _configurations.stream().map(c->c.getClass().getName()).toArray(String[]::new);
}
public void sort()
{
sort(_configurations);
if (LOG.isDebugEnabled())
{
for (Configuration c: _configurations)
LOG.debug("sorted {}",c);
}
}
public static void sort(List<Configuration> configurations)
{
// Sort the configurations
Map<String,Configuration> by_name = new HashMap<>();
Map<String,List<Configuration>> replaced_by = new HashMap<>();
TopologicalSort<Configuration> sort = new TopologicalSort<>();
for (Configuration c:configurations)
{
by_name.put(c.getClass().getName(),c);
if (c.replaces()!=null)
replaced_by.computeIfAbsent(c.replaces().getName(),key->new ArrayList<>()).add(c);
}
for (Configuration c:configurations)
{
for (String b:c.getDependencies())
{
Configuration before=by_name.get(b);
if (before!=null)
sort.addBeforeAfter(before,c);
if (replaced_by.containsKey(b))
replaced_by.get(b).forEach(bc->sort.addBeforeAfter(bc,c));
}
for (String a:c.getDependents())
{
Configuration after=by_name.get(a);
if (after!=null)
sort.addBeforeAfter(c,after);
if (replaced_by.containsKey(a))
replaced_by.get(a).forEach(ac->sort.addBeforeAfter(c,ac));
}
}
sort.sort(configurations);
}
public List<Configuration> getConfigurations()
{
return Collections.unmodifiableList(_configurations);
}
@Override
public Configuration get(int index)
{
return _configurations.get(index);
}
@Override
public Iterator<Configuration> iterator()
{
return getConfigurations().iterator();
}
private void addConfiguration(Configuration configuration)
{
String name=configuration.getClass().getName();
// Is this configuration known?
if (LOG.isDebugEnabled())
{
if (!__knownByClassName.contains(name))
LOG.warn("Unknown configuration {}. Not declared for ServiceLoader!",name);
}
// Do we need to replace any existing configuration?
Class<? extends Configuration> replaces = configuration.replaces();
if (replaces!=null)
{
for (ListIterator<Configuration> i=_configurations.listIterator();i.hasNext();)
{
Configuration c=i.next();
if(c.getClass().getName().equals(replaces.getName())
|| c.replaces()!=null && c.replaces().getName().equals(replaces.getName()))
{
i.set(configuration);
return;
}
}
_configurations.add(configuration);
return;
}
if (!_configurations.stream().map(c->c.getClass().getName()).anyMatch(n->{return name.equals(n);}))
_configurations.add(configuration);
}
@Override
public String toString()
{
return getConfigurations().toString();
}
public void preConfigure(WebAppContext webapp) throws Exception
{
// Configure webapp
// iterate with index to allows changes to the Configurations
// during calls to preConfiguration.
for (int i=0; i<_configurations.size() ;i++)
{
Configuration configuration=_configurations.get(i);
LOG.debug("preConfigure with {}",configuration);
configuration.preConfigure(webapp);
if (_configurations.get(i)!=configuration)
throw new ConcurrentModificationException("Cannot change prior configuration");
}
}
/**
* @param webapp The webapp to configure
* @return false if a {@link Configuration#abort(WebAppContext)} returns true, true otherwise
* @throws Exception Thrown by {@link Configuration#configure(WebAppContext)}
*/
public boolean configure(WebAppContext webapp) throws Exception
{
// Configure webapp
for (Configuration configuration : _configurations)
{
LOG.debug("configure {}",configuration);
configuration.configure(webapp);
if (configuration.abort(webapp))
return false;
}
return true;
}
public void postConfigure(WebAppContext webapp) throws Exception
{
// Configure webapp
for (Configuration configuration : _configurations)
{
LOG.debug("postConfigure {}",configuration);
configuration.postConfigure(webapp);
}
}
}