You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by lo...@apache.org on 2020/06/05 08:58:58 UTC

[myfaces-tobago] branch tobago-4.x updated: Reimplement tobago-config sort with topological sorting

This is an automated email from the ASF dual-hosted git repository.

lofwyr pushed a commit to branch tobago-4.x
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git


The following commit(s) were added to refs/heads/tobago-4.x by this push:
     new 95a607b  Reimplement tobago-config sort with topological sorting
95a607b is described below

commit 95a607bec903631f01f6a9b2e49fcc314ed51d35
Author: Udo Schnurpfeil <ud...@irian.eu>
AuthorDate: Fri Jun 5 10:37:45 2020 +0200

    Reimplement tobago-config sort with topological sorting
    
    resolves: TOBAGO-2040
    
    (cherry picked from commit 3d6c8d563eb987639c4638bfc03bb555361bcd5b)
---
 .../internal/config/TobagoConfigBuilder.java       |   4 +-
 .../tobago/internal/config/TobagoConfigMerger.java | 148 +++++++++
 .../tobago/internal/config/TobagoConfigSorter.java | 358 ++++++++-------------
 .../config/TobagoConfigMergingUnitTest.java        |  14 +-
 .../config/TobagoConfigSorterUnitTest.java         | 209 +++++++++---
 5 files changed, 456 insertions(+), 277 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigBuilder.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigBuilder.java
index 3545444..bac668a 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigBuilder.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigBuilder.java
@@ -83,8 +83,8 @@ public class TobagoConfigBuilder {
     configFromClasspath();
     configFromWebInf();
     final TobagoConfigSorter sorter = new TobagoConfigSorter(configFragmentList);
-    sorter.sort();
-    return sorter.merge();
+    final TobagoConfigMerger merger = new TobagoConfigMerger(sorter.topologicalSort());
+    return merger.merge();
   }
 
   private void configFromWebInf()
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMerger.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMerger.java
new file mode 100644
index 0000000..e6c54bf
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMerger.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.internal.config;
+
+import org.apache.myfaces.tobago.context.ThemeImpl;
+import org.apache.myfaces.tobago.sanitizer.IgnoringSanitizer;
+import org.apache.myfaces.tobago.sanitizer.JsoupSanitizer;
+import org.apache.myfaces.tobago.sanitizer.Sanitizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+public class TobagoConfigMerger {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final List<TobagoConfigFragment> list;
+
+  public TobagoConfigMerger(final List<TobagoConfigFragment> list) {
+    this.list =    list;
+  }
+
+  public TobagoConfigImpl merge() {
+
+    final TobagoConfigImpl result = new TobagoConfigImpl();
+
+    // default sanitizer
+    String sanitizerClass = JsoupSanitizer.class.getName();
+    Properties sanitizerProperties = new Properties();
+    sanitizerProperties.setProperty("whitelist", "relaxed");
+
+    for (TobagoConfigFragment fragment : list) {
+
+      // default theme
+      final String defaultTheme = fragment.getDefaultThemeName();
+      if (defaultTheme != null) {
+        result.setDefaultThemeName(defaultTheme);
+      }
+
+      // supported themes
+      for (final String supported : fragment.getSupportedThemeNames()) {
+        result.addSupportedThemeName(supported);
+      }
+
+      // session secret
+      if (fragment.getCreateSessionSecret() != null) {
+        result.setCreateSessionSecret(fragment.getCreateSessionSecret());
+      }
+      if (fragment.getCheckSessionSecret() != null) {
+        result.setCheckSessionSecret(fragment.getCheckSessionSecret());
+      }
+
+      if (fragment.getPreventFrameAttacks() != null) {
+        result.setPreventFrameAttacks(fragment.getPreventFrameAttacks());
+      }
+
+      if (fragment.getContentSecurityPolicy() != null) {
+        result.getContentSecurityPolicy().merge(fragment.getContentSecurityPolicy());
+      }
+
+      if (fragment.getSecurityAnnotation() != null) {
+        result.setSecurityAnnotation(fragment.getSecurityAnnotation());
+      }
+
+      if (fragment.getSetNosniffHeader() != null) {
+        result.setSetNosniffHeader(fragment.getSetNosniffHeader());
+      }
+
+      if (fragment.getSanitizerClass() != null) {
+        sanitizerClass = fragment.getSanitizerClass();
+        sanitizerProperties = fragment.getSanitizerProperties();
+      }
+
+      if (fragment.getDecodeLineFeed() != null) {
+        result.setDecodeLineFeed(fragment.getDecodeLineFeed());
+      }
+
+      // theme definition
+      for (final ThemeImpl theme : fragment.getThemeDefinitions()) {
+        result.addAvailableTheme(theme);
+      }
+
+      // url
+      // todo???
+
+      final Map<String, String> mimeTypes = result.getMimeTypes();
+      for (final Map.Entry<String, String> entry : fragment.getMimeTypes().entrySet()) {
+        mimeTypes.put(entry.getKey(), entry.getValue());
+      }
+
+    }
+
+    resolveThemes(result, result.getAvailableThemes());
+
+    if (sanitizerClass != null) {
+      try {
+        final Class<? extends Sanitizer> aClass = Class.forName(sanitizerClass).asSubclass(Sanitizer.class);
+        final Sanitizer sanitizer = aClass.newInstance();
+        sanitizer.setProperties(sanitizerProperties);
+        result.setSanitizer(sanitizer);
+      } catch (final Exception e) {
+        LOG.error("Can't create sanitizer: '" + sanitizerClass + "'", e);
+        result.setSanitizer(new IgnoringSanitizer());
+      }
+    }
+
+    return result;
+  }
+
+  private void resolveThemes(final TobagoConfigImpl tobagoConfig, final Map<String, ThemeImpl> map) {
+    for (final ThemeImpl theme : map.values()) {
+      final String fallbackName = theme.getFallbackName();
+      final ThemeImpl fallback = map.get(fallbackName);
+      theme.setFallback(fallback);
+    }
+    for (final ThemeImpl theme : map.values()) {
+      theme.resolveFallbacks();
+    }
+    for (final ThemeImpl theme : map.values()) {
+      theme.resolveResources();
+    }
+    for (final ThemeImpl theme : map.values()) {
+      theme.init();
+    }
+  }
+
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorter.java
index fdb0ec7..58ded3d 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorter.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorter.java
@@ -19,309 +19,203 @@
 
 package org.apache.myfaces.tobago.internal.config;
 
-import org.apache.myfaces.tobago.context.ThemeImpl;
-import org.apache.myfaces.tobago.exception.TobagoConfigurationException;
-import org.apache.myfaces.tobago.sanitizer.IgnoringSanitizer;
-import org.apache.myfaces.tobago.sanitizer.JsoupSanitizer;
-import org.apache.myfaces.tobago.sanitizer.Sanitizer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
-import java.util.Properties;
 
-public class TobagoConfigSorter implements Comparator<TobagoConfigFragment> {
+public class TobagoConfigSorter {
 
   private static final Logger LOG = LoggerFactory.getLogger(TobagoConfigSorter.class);
 
-  private List<TobagoConfigFragment> list;
-  private List<Pair> pairs;
+  private final List<Vertex> vertices = new ArrayList<>();
 
-  public TobagoConfigSorter(final List<TobagoConfigFragment> list) {
-    this.list = list;
-  }
-
-  public void sort() {
-
-    createRelevantPairs();
-
-    makeTransitive();
-
-    ensureIrreflexive();
-
-    ensureAntiSymmetric();
-
-    sort0();
-
-    if (LOG.isInfoEnabled()) {
-      LOG.info("Order of the Tobago config files:");
-      for (final TobagoConfigFragment fragment : list) {
-        String name = fragment.getName();
-        if (name == null) {
-          name = "<unnamed>";
-        } else {
-          name = "'" + name + "'";
-        }
-        LOG.info("name=" + name + " url='" + fragment.getUrl() + "'");
-      }
+  public TobagoConfigSorter(final List<TobagoConfigFragment> fragmentList) {
+    for (TobagoConfigFragment tobagoConfigFragment : fragmentList) {
+      vertices.add(new Vertex(tobagoConfigFragment));
     }
   }
 
-  public TobagoConfigImpl merge() {
-
-    final TobagoConfigImpl result = new TobagoConfigImpl();
-
-    // default sanitizer
-    String sanitizerClass = JsoupSanitizer.class.getName();
-    Properties sanitizerProperties = new Properties();
-    sanitizerProperties.setProperty("whitelist", "relaxed");
-
-    for (final TobagoConfigFragment fragment : list) {
-      // default theme
-      final String defaultTheme = fragment.getDefaultThemeName();
-      if (defaultTheme != null) {
-        result.setDefaultThemeName(defaultTheme);
-      }
-
-      // supported themes
-      for (final String supported : fragment.getSupportedThemeNames()) {
-        result.addSupportedThemeName(supported);
-      }
-
-      // session secret
-      if (fragment.getCreateSessionSecret() != null) {
-        result.setCreateSessionSecret(fragment.getCreateSessionSecret());
-      }
-      if (fragment.getCheckSessionSecret() != null) {
-        result.setCheckSessionSecret(fragment.getCheckSessionSecret());
-      }
-
-      if (fragment.getPreventFrameAttacks() != null) {
-        result.setPreventFrameAttacks(fragment.getPreventFrameAttacks());
-      }
-
-      if (fragment.getContentSecurityPolicy() != null) {
-        result.getContentSecurityPolicy().merge(fragment.getContentSecurityPolicy());
-      }
+  /**
+   * Topological sorting with setup and cycle check.
+   *
+   * @throws IllegalStateException When detecting a cycle.
+   */
+  public List<TobagoConfigFragment> topologicalSort() {
 
-      if (fragment.getSecurityAnnotation() != null) {
-        result.setSecurityAnnotation(fragment.getSecurityAnnotation());
-      }
+    createEdges();
+    checkCycles();
 
-      if (fragment.getSetNosniffHeader() != null) {
-        result.setSetNosniffHeader(fragment.getSetNosniffHeader());
-      }
-
-      if (fragment.getSanitizerClass() != null) {
-        sanitizerClass = fragment.getSanitizerClass();
-        sanitizerProperties = fragment.getSanitizerProperties();
-      }
-
-      if (fragment.getDecodeLineFeed() != null) {
-        result.setDecodeLineFeed(fragment.getDecodeLineFeed());
-      }
-
-      // theme definition
-      for (final ThemeImpl theme : fragment.getThemeDefinitions()) {
-        result.addAvailableTheme(theme);
-      }
-
-      // url
-      // todo???
-
-      final Map<String, String> mimeTypes = result.getMimeTypes();
-      for (final Map.Entry<String, String> entry : fragment.getMimeTypes().entrySet()) {
-        mimeTypes.put(entry.getKey(), entry.getValue());
-      }
+    List<TobagoConfigFragment> result = new ArrayList<>();
 
+    for (Vertex vertex : vertices) {
+      topologicalSort0(vertex, result);
     }
 
-    resolveThemes(result, result.getAvailableThemes());
-
-    if (sanitizerClass != null) {
-      try {
-        final Class<? extends Sanitizer> aClass = Class.forName(sanitizerClass).asSubclass(Sanitizer.class);
-        final Sanitizer sanitizer = aClass.newInstance();
-        sanitizer.setProperties(sanitizerProperties);
-        result.setSanitizer(sanitizer);
-      } catch (final Exception e) {
-        LOG.error("Can't create sanitizer: '" + sanitizerClass + "'", e);
-        result.setSanitizer(new IgnoringSanitizer());
-      }
-    }
+    logResult(result);
 
     return result;
   }
 
-  protected void makeTransitive() {
-    // make the half order transitive: a < b && b < c => a < c
-    boolean growing = true;
-    while (growing) {
-      growing = false;
-      for (int i = 0; i < pairs.size(); i++) {
-        for (int j = 0; j < pairs.size(); j++) {
-          if (pairs.get(i).getHigher() == pairs.get(j).getLower()
-              && !isInRelation(pairs.get(i).getLower(), pairs.get(j).getHigher())) {
-            pairs.add(new Pair(pairs.get(i).getLower(), pairs.get(j).getHigher()));
-            growing = true;
-          }
-        }
-      }
+  /**
+   * Internal recursive method for the topological sort.
+   */
+  private void topologicalSort0(Vertex vertex, List<TobagoConfigFragment> result) {
+    if (vertex.isVisited()) {
+      return;
     }
-  }
 
-  protected void ensureIrreflexive() {
-    for (final Pair a : pairs) {
-        if (a.getLower() == a.getHigher()) {
-          final StringBuilder buffer = new StringBuilder();
-          buffer.append("Ordering problem. There are conflicting order rules. Not irreflexive. '");
-          buffer.append(a.getLower());
-          buffer.append("' < '");
-          buffer.append(a.getHigher());
-          buffer.append("'!\nThe reason may be a cycle.\n");
-          buffer.append("Complete list of rules: \n");
-          for (final Pair pair : pairs) {
-            buffer.append("'");
-            buffer.append(pair.getLower());
-            buffer.append("' < '");
-            buffer.append(pair.getHigher());
-            buffer.append("'\n");
+    vertex.setVisited(true);
 
-          }
-          throw new TobagoConfigurationException(buffer.toString());
-        }
-      }
-  }
+    // recursion for all vertices adjacent to this vertex
+    for (Vertex adjacent : vertex.getAdjacencyList()) {
+      topologicalSort0(adjacent, result);
+    }
 
-  protected void ensureAntiSymmetric() {
-    for (final Pair a : pairs) {
-      for (final Pair b : pairs) {
-        if (a.getLower() == b.getHigher() && a.getHigher() == b.getLower()) {
-          final StringBuilder buffer = new StringBuilder();
-          buffer.append("Ordering problem. There are conflicting order rules. Not antisymmetric. '");
-          buffer.append(a.getLower());
-          buffer.append("' < '");
-          buffer.append(a.getHigher());
-          buffer.append("'" + "'");
-          buffer.append(a.getLower());
-          buffer.append("' > '");
-          buffer.append(a.getHigher());
-          buffer.append("'!\nThe reason may be a cycle.\n");
-          buffer.append("Complete list of rules: \n");
-          for (final Pair pair : pairs) {
-            buffer.append("'");
-            buffer.append(pair.getLower());
-            buffer.append("' < '");
-            buffer.append(pair.getHigher());
-            buffer.append("'\n");
+    result.add(vertex.getFragment());
+  }
 
+  private void logResult(List<TobagoConfigFragment> result) {
+    if (LOG.isInfoEnabled()) {
+      StringBuilder builder = new StringBuilder("Order of the Tobago config files: ");
+      for (TobagoConfigFragment fragment : result) {
+        final String name = fragment.getName();
+        if (LOG.isDebugEnabled()) {
+          builder.append("name=");
+          if (name == null) {
+            builder.append("<unnamed>");
+          } else {
+            builder.append("'");
+            builder.append(name);
+            builder.append("'");
           }
-          throw new TobagoConfigurationException(buffer.toString());
+          builder.append(" url='");
+          builder.append(fragment.getUrl());
+          builder.append("'");
+        } else {
+          builder.append(name);
+          builder.append(", ");
         }
       }
+      LOG.info(builder.toString());
     }
   }
 
-  @Override
-  public int compare(final TobagoConfigFragment a, final TobagoConfigFragment b) {
-    if (isInRelation(a, b)) {
-      return -1;
-    }
-    if (isInRelation(b, a)) {
-      return 1;
-    }
-    return 0;
-  }
 
-  protected void createRelevantPairs() {
-
-    pairs = new ArrayList<>();
+  private void createEdges() {
 
     // collecting all relations, which are relevant for us. We don't need "before" and "after" of unknown names.
-    for (final TobagoConfigFragment tobagoConfig : list) {
-      for (final String befores : tobagoConfig.getBefore()) {
+    for (final Vertex vertex : vertices) {
+      final TobagoConfigFragment current = vertex.getFragment();
+
+      for (final String befores : current.getBefore()) {
         final TobagoConfigFragment before = findByName(befores);
         if (before != null) {
-          pairs.add(new Pair(tobagoConfig, before));
+          findVertex(before).addAdjacent(findVertex(current));
         }
       }
-      for (final String afters : tobagoConfig.getAfter()) {
+      for (final String afters : current.getAfter()) {
         final TobagoConfigFragment after = findByName(afters);
         if (after != null) {
-          pairs.add(new Pair(after, tobagoConfig));
+          findVertex(current).addAdjacent(findVertex(after));
         }
       }
     }
   }
 
-  protected void sort0() {
-    list.sort(this);
+  /**
+   * Cycle detection: if the base in reachable form its own, than there is a cycle.
+   *
+   * @throws IllegalStateException When detecting a cycle.
+   */
+  private void checkCycles() {
+    LOG.debug("Cycle detection:");
+    for (Vertex vertex : vertices) {
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Checking reachable vertices from base {}", vertex.getFragment());
+      }
+      checkCycles0(vertex, vertex);
+    }
   }
 
-  private boolean isInRelation(final TobagoConfigFragment lower, final TobagoConfigFragment higher) {
-    for (final Pair p : pairs) {
-      if (p.getLower() == lower && p.getHigher() == higher) {
-        return true;
+  private void checkCycles0(final Vertex vertex, final Vertex base) {
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("vertex: {}", vertex.toString());
+      LOG.debug("base:   {}", base.getFragment().toString());
+    }
+    for (Vertex adjacent : vertex.getAdjacencyList()) {
+      if (base == adjacent) {
+        throw new IllegalStateException("Cycle detected name='" + vertex + "' base=" + base + "! ");
       }
+      checkCycles0(adjacent, base);
     }
-    return false;
   }
 
-  private TobagoConfigFragment findByName(final String name) {
-    for (final TobagoConfigFragment tobagoConfig : list) {
-      if (name.equals(tobagoConfig.getName())) {
-        return tobagoConfig;
+  private Vertex findVertex(final TobagoConfigFragment fragment) {
+    for (Vertex vertex : vertices) {
+      if (vertex.getFragment() == fragment) {
+        return vertex;
       }
     }
-    return null;
+    throw new RuntimeException("Problem with sorting! This might be a bug.");
   }
 
-  private void resolveThemes(final TobagoConfigImpl tobagoConfig, final Map<String, ThemeImpl> map) {
-    for (final ThemeImpl theme : map.values()) {
-      final String fallbackName = theme.getFallbackName();
-      final ThemeImpl fallback = map.get(fallbackName);
-      theme.setFallback(fallback);
-    }
-    for (final ThemeImpl theme : map.values()) {
-      theme.resolveFallbacks();
-    }
-    for (final ThemeImpl theme : map.values()) {
-      theme.resolveResources();
-    }
-    for (final ThemeImpl theme : map.values()) {
-      theme.init();
+  private TobagoConfigFragment findByName(final String name) {
+    for (final Vertex vertex : vertices) {
+      TobagoConfigFragment fragment = vertex.getFragment();
+      if (name.equals(fragment.getName())) {
+        return fragment;
+      }
     }
+    return null;
   }
 
-  protected List<Pair> getPairs() {
-    return pairs;
-  }
+  private static class Vertex {
 
-  private static class Pair {
+    private final TobagoConfigFragment fragment;
+    private final List<Vertex> adjacencyList;
+    private boolean visited;
 
-    private final TobagoConfigFragment lower;
-    private final TobagoConfigFragment higher;
+    private Vertex(final TobagoConfigFragment fragment) {
+      this.fragment = fragment;
+      this.adjacencyList = new ArrayList<>();
+    }
 
-    private Pair(final TobagoConfigFragment lower, final TobagoConfigFragment higher) {
-      this.lower = lower;
-      this.higher = higher;
+    public boolean isVisited() {
+      return visited;
     }
 
-    public TobagoConfigFragment getLower() {
-      return lower;
+    public void setVisited(boolean visited) {
+      this.visited = visited;
     }
 
-    public TobagoConfigFragment getHigher() {
-      return higher;
+    public TobagoConfigFragment getFragment() {
+      return fragment;
+    }
+
+    public void addAdjacent(Vertex higher) {
+      adjacencyList.add(higher);
+    }
+
+    public List<Vertex> getAdjacencyList() {
+      return adjacencyList;
     }
 
     @Override
     public String toString() {
-      return lower + "<" + higher;
+      StringBuilder builder = new StringBuilder();
+      builder.append(fragment);
+      builder.append(" -> [");
+      for (Vertex vertex : adjacencyList) {
+        builder.append(vertex.getFragment());
+        builder.append(", ");
+      }
+      if (builder.charAt(builder.length() - 1) == ' ') {
+        builder.delete(builder.length() - 2, builder.length());
+      }
+      builder.append("]");
+      return builder.toString();
     }
   }
-
 }
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMergingUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMergingUnitTest.java
index a9566bc..c9482a2 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMergingUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigMergingUnitTest.java
@@ -69,7 +69,7 @@ public class TobagoConfigMergingUnitTest {
     final TobagoConfigImpl config = loadAndMerge(
         "tobago-config-merge-0.xml");
 
-    Assert.assertTrue(config.getContentSecurityPolicy().getMode() == ContentSecurityPolicy.Mode.ON);
+    Assert.assertSame(config.getContentSecurityPolicy().getMode(), ContentSecurityPolicy.Mode.ON);
     final Map<String, String> directiveMap = config.getContentSecurityPolicy().getDirectiveMap();
     Assert.assertEquals(1, directiveMap.size());
     Assert.assertEquals("'self'", directiveMap.get("default-src"));
@@ -83,7 +83,7 @@ public class TobagoConfigMergingUnitTest {
         "tobago-config-merge-0.xml",
         "tobago-config-merge-1.xml");
 
-    Assert.assertTrue(config.getContentSecurityPolicy().getMode() == ContentSecurityPolicy.Mode.REPORT_ONLY);
+    Assert.assertSame(config.getContentSecurityPolicy().getMode(), ContentSecurityPolicy.Mode.REPORT_ONLY);
     final Map<String, String> directiveMap = config.getContentSecurityPolicy().getDirectiveMap();
     Assert.assertEquals(2, directiveMap.size());
     Assert.assertEquals("'self'", directiveMap.get("default-src"));
@@ -99,7 +99,7 @@ public class TobagoConfigMergingUnitTest {
         "tobago-config-merge-1.xml",
         "tobago-config-merge-2.xml");
 
-    Assert.assertTrue(config.getContentSecurityPolicy().getMode() == ContentSecurityPolicy.Mode.OFF);
+    Assert.assertSame(config.getContentSecurityPolicy().getMode(), ContentSecurityPolicy.Mode.OFF);
     Assert.assertEquals(2, config.getContentSecurityPolicy().getDirectiveMap().size());
   }
 
@@ -111,7 +111,7 @@ public class TobagoConfigMergingUnitTest {
         "tobago-config-merge-0.xml",
         "tobago-config-merge-3.xml");
 
-    Assert.assertTrue(config.getContentSecurityPolicy().getMode() == ContentSecurityPolicy.Mode.ON);
+    Assert.assertSame(config.getContentSecurityPolicy().getMode(), ContentSecurityPolicy.Mode.ON);
     final Map<String, String> directiveMap = config.getContentSecurityPolicy().getDirectiveMap();
     Assert.assertEquals(1, directiveMap.size());
     Assert.assertEquals("'self' https:", directiveMap.get("default-src"));
@@ -127,7 +127,7 @@ public class TobagoConfigMergingUnitTest {
         "tobago-config-merge-2.xml");
 
     final Map<String, String> mimeTypes = config.getMimeTypes();
-    Assert.assertTrue(mimeTypes.size() == 3);
+    Assert.assertEquals(3, mimeTypes.size());
     Assert.assertEquals("test/one", mimeTypes.get("test-1"));
     Assert.assertEquals("test/zwei", mimeTypes.get("test-2"));
     Assert.assertEquals("test/three", mimeTypes.get("test-3"));
@@ -145,7 +145,7 @@ public class TobagoConfigMergingUnitTest {
     }
 
     final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
-    sorter.sort();
-    return sorter.merge();
+    final TobagoConfigMerger merger = new TobagoConfigMerger(sorter.topologicalSort());
+    return merger.merge();
   }
 }
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorterUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorterUnitTest.java
index 61b9c7f..30b1c23 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorterUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/TobagoConfigSorterUnitTest.java
@@ -21,12 +21,17 @@ package org.apache.myfaces.tobago.internal.config;
 
 import org.junit.Assert;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.List;
 
 public class TobagoConfigSorterUnitTest {
 
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
   @Test
   public void testCompare() {
 
@@ -95,35 +100,85 @@ public class TobagoConfigSorterUnitTest {
     list.add(n);
 
     final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
-    sorter.createRelevantPairs();
+    final List<TobagoConfigFragment> result = sorter.topologicalSort();
+
+    Assert.assertEquals(a, result.get(0));
+    Assert.assertEquals(b, result.get(1));
+    Assert.assertEquals(c, result.get(2));
+    Assert.assertEquals(u1, result.get(3));
+    Assert.assertEquals(u2, result.get(4));
+    Assert.assertEquals(d, result.get(5));
+    Assert.assertEquals(e, result.get(6));
+    Assert.assertEquals(f, result.get(7));
+    Assert.assertEquals(u3, result.get(8));
+    Assert.assertEquals(m, result.get(9));
+    Assert.assertEquals(n, result.get(10));
+  }
+
+  @Test
+  public void test0() {
+
+    // config + names
+
+    final List<TobagoConfigFragment> list = new ArrayList<>();
+
+    final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
+
+    final List<TobagoConfigFragment> result = sorter.topologicalSort();
+
+    Assert.assertTrue(result.isEmpty());
+  }
+
+  @Test
+  public void testCycle1Before() {
+
+    // config + names
+
+    final TobagoConfigFragment a = new TobagoConfigFragment();
+    a.setName("a");
+
+    a.getBefore().add("a");
+
+    final List<TobagoConfigFragment> list = new ArrayList<>();
+    list.add(a);
+
+    final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
+
+    try {
+      sorter.topologicalSort();
+
+      Assert.fail("Cycle was not detected!");
+    } catch (final RuntimeException e) {
+      LOG.info("Success: Cycle found: {}", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCycle1After() {
 
-    Assert.assertEquals(9, sorter.getPairs().size()); // all but these with "z" and "y"
+    // config + names
 
-    sorter.makeTransitive();
+    final TobagoConfigFragment a = new TobagoConfigFragment();
+    a.setName("a");
 
-    Assert.assertEquals(28, sorter.getPairs().size());
+    a.getAfter().add("a");
 
-    sorter.ensureIrreflexive();
+    final List<TobagoConfigFragment> list = new ArrayList<>();
+    list.add(a);
 
-    sorter.ensureAntiSymmetric();
+    final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
 
-    sorter.sort0();
+    try {
+      sorter.topologicalSort();
 
-    Assert.assertEquals(a, list.get(0));
-    Assert.assertEquals(b, list.get(1));
-    Assert.assertEquals(c, list.get(2));
-    Assert.assertEquals(u1, list.get(3));
-    Assert.assertEquals(u2, list.get(4));
-    Assert.assertEquals(d, list.get(5));
-    Assert.assertEquals(e, list.get(6));
-    Assert.assertEquals(f, list.get(7));
-    Assert.assertEquals(u3, list.get(8));
-    Assert.assertEquals(m, list.get(9));
-    Assert.assertEquals(n, list.get(10));
+      Assert.fail("Cycle was not detected!");
+    } catch (final RuntimeException e) {
+      LOG.info("Success: Cycle found: {}", e.getMessage());
+    }
   }
 
   @Test
-  public void testCycle() {
+  public void testCycle2() {
 
     // config + names
 
@@ -142,24 +197,18 @@ public class TobagoConfigSorterUnitTest {
     list.add(b);
 
     final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
-    sorter.createRelevantPairs();
-
-    Assert.assertEquals(2, sorter.getPairs().size()); // all but these with "z" and "y"
-
-    sorter.makeTransitive();
 
     try {
-      sorter.ensureIrreflexive();
-      sorter.ensureAntiSymmetric();
+      sorter.topologicalSort();
 
-      Assert.fail("Cycle was not found");
+      Assert.fail("Cycle was not detected!");
     } catch (final RuntimeException e) {
-      // must find the cycle
+      LOG.info("Success: Cycle found: {}", e.getMessage());
     }
   }
 
   @Test
-  public void testCycle2() {
+  public void testCycle2BeforeAfter() {
 
     // config + names
 
@@ -179,19 +228,107 @@ public class TobagoConfigSorterUnitTest {
     list.add(b);
 
     final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
-    sorter.createRelevantPairs();
+    try {
+      sorter.topologicalSort();
 
-    Assert.assertEquals(2, sorter.getPairs().size()); // all but these with "z" and "y"
+      Assert.fail("Cycle was not detected!");
+    } catch (final RuntimeException e) {
+      LOG.info("Success: Cycle found: {}", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testCycle3() {
+
+    // config + names
+
+    final TobagoConfigFragment a = new TobagoConfigFragment();
+    a.setName("a");
+
+    final TobagoConfigFragment b = new TobagoConfigFragment();
+    b.setName("b");
+
+    final TobagoConfigFragment c = new TobagoConfigFragment();
+    c.setName("c");
+
+    // before
+    a.getBefore().add("b");
+    b.getBefore().add("c");
+    a.getAfter().add("c");
 
-    sorter.makeTransitive();
+    final List<TobagoConfigFragment> list = new ArrayList<>();
+    list.add(a);
+    list.add(b);
+    list.add(c);
+
+    final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
 
     try {
-      sorter.ensureIrreflexive();
-      sorter.ensureAntiSymmetric();
+      sorter.topologicalSort();
 
-      Assert.fail("Cycle was not found");
+      Assert.fail("Cycle was not detected!");
     } catch (final RuntimeException e) {
-      // must find the cycle
+      LOG.info("Success: Cycle found: {}", e.getMessage());
     }
   }
+
+  @Test
+  public void testReal() {
+
+    // config + names
+
+    final TobagoConfigFragment blank = new TobagoConfigFragment();
+    blank.setName("tobago-example-blank");
+
+    final TobagoConfigFragment charlotteville = new TobagoConfigFragment();
+    charlotteville.setName("tobago-theme-charlotteville");
+
+    final TobagoConfigFragment roxborough = new TobagoConfigFragment();
+    roxborough.setName("tobago-theme-roxborough");
+
+    final TobagoConfigFragment scarborough = new TobagoConfigFragment();
+    scarborough.setName("tobago-theme-scarborough");
+
+    final TobagoConfigFragment speyside = new TobagoConfigFragment();
+    speyside.setName("tobago-theme-speyside");
+
+    final TobagoConfigFragment standard = new TobagoConfigFragment();
+    standard.setName("tobago-theme-standard");
+
+    final TobagoConfigFragment core = new TobagoConfigFragment();
+    core.setName("tobago-core");
+
+    // after
+    blank.getAfter().add(speyside.getName());
+    blank.getAfter().add(scarborough.getName());
+    blank.getAfter().add(roxborough.getName());
+    blank.getAfter().add(standard.getName());
+    blank.getAfter().add(charlotteville.getName());
+    charlotteville.getAfter().add(standard.getName());
+    roxborough.getAfter().add(standard.getName());
+    scarborough.getAfter().add(standard.getName());
+    speyside.getAfter().add(standard.getName());
+    standard.getAfter().add(core.getName());
+
+
+    final List<TobagoConfigFragment> list = new ArrayList<>();
+    list.add(blank);
+    list.add(charlotteville);
+    list.add(roxborough);
+    list.add(scarborough);
+    list.add(speyside);
+    list.add(standard);
+    list.add(core);
+
+    final TobagoConfigSorter sorter = new TobagoConfigSorter(list);
+
+    final List<TobagoConfigFragment> result = sorter.topologicalSort();
+
+    Assert.assertEquals(core, result.get(0));
+    Assert.assertEquals(standard, result.get(1));
+    Assert.assertEquals(blank, result.get(6));
+    final int blankPos = result.indexOf(blank);
+    final int speysidePos = result.indexOf(speyside);
+    Assert.assertTrue(blankPos > speysidePos);
+  }
 }