/* * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.yangtools.yang.parser.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil; import org.opendaylight.yangtools.yang.common.YangVersion; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.ModuleImport; import org.opendaylight.yangtools.yang.parser.util.TopologicalSort.Node; import org.opendaylight.yangtools.yang.parser.util.TopologicalSort.NodeImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Creates a module dependency graph from provided {@link Module}s and * provides a {@link #sort(Module...)} method. It is topological sort and * returns modules in order in which they should be processed (e.g. if A imports * B, sort returns {B, A}). */ public final class ModuleDependencySort { private static final Date DEFAULT_REVISION = SimpleDateFormatUtil.DEFAULT_DATE_REV; private static final Logger LOGGER = LoggerFactory.getLogger(ModuleDependencySort.class); private static final Function<Node, Module> TOPOLOGY_FUNCTION = input -> { if (input == null) { return null; } return ((ModuleNodeImpl) input).getReference(); }; /** * It is not desirable to instance this class */ private ModuleDependencySort() { } /** * Topological sort of module dependency graph. * * @param modules YANG modules * @return Sorted list of Modules. Modules can be further processed in * returned order. */ public static List<Module> sort(final Module... modules) { final List<TopologicalSort.Node> sorted = sortInternal(Arrays.asList(modules)); // Cast to Module from Node and return return Lists.transform(sorted, TOPOLOGY_FUNCTION); } private static List<TopologicalSort.Node> sortInternal(final Iterable<Module> modules) { final Map<String, Map<Date, ModuleNodeImpl>> moduleGraph = createModuleGraph(modules); final Set<TopologicalSort.Node> nodes = Sets.newHashSet(); for (final Map<Date, ModuleNodeImpl> map : moduleGraph.values()) { for (final ModuleNodeImpl node : map.values()) { nodes.add(node); } } return TopologicalSort.sort(nodes); } @VisibleForTesting static Map<String, Map<Date, ModuleNodeImpl>> createModuleGraph(final Iterable<Module> builders) { final Map<String, Map<Date, ModuleNodeImpl>> moduleGraph = Maps.newHashMap(); processModules(moduleGraph, builders); processDependencies(moduleGraph, builders); return moduleGraph; } /** * Extract module:revision from module builders */ private static void processDependencies(final Map<String, Map<Date, ModuleNodeImpl>> moduleGraph, final Iterable<Module> mmbs) { final Map<URI, Module> allNS = new HashMap<>(); // Create edges in graph for (final Module module : mmbs) { final Map<String, Date> imported = Maps.newHashMap(); String fromName; Date fromRevision; Collection<ModuleImport> imports; URI ns; fromName = module.getName(); fromRevision = module.getRevision(); imports = module.getImports(); ns = module.getNamespace(); // check for existence of module with same namespace if (allNS.containsKey(ns)) { final Module mod = allNS.get(ns); final String name = mod.getName(); final Date revision = mod.getRevision(); if (!(fromName.equals(name))) { LOGGER.warn( "Error while sorting module [{}, {}]: module with same namespace ({}) already loaded: [{}, {}]", fromName, fromRevision, ns, name, revision); } } else { allNS.put(ns, module); } // no need to check if other Type of object, check is performed in // process modules if (fromRevision == null) { fromRevision = DEFAULT_REVISION; } for (final ModuleImport imprt : imports) { final String toName = imprt.getModuleName(); final Date toRevision = imprt.getRevision() == null ? DEFAULT_REVISION : imprt.getRevision(); final ModuleNodeImpl from = moduleGraph.get(fromName).get(fromRevision); final ModuleNodeImpl to = getModuleByNameAndRevision(moduleGraph, fromName, fromRevision, toName, toRevision); /* * If it is an yang 1 module, check imports: If module is imported twice with different * revisions then throw exception */ if (YangVersion.VERSION_1.toString().equals(module.getYangVersion()) && imported.get(toName) != null && !imported.get(toName).equals(toRevision) && !imported.get(toName).equals(DEFAULT_REVISION) && !toRevision.equals(DEFAULT_REVISION)) { ex(String.format("Module:%s imported twice with different revisions:%s, %s", toName, formatRevDate(imported.get(toName)), formatRevDate(toRevision))); } imported.put(toName, toRevision); from.addEdge(to); } } } /** * Get imported module by its name and revision from moduleGraph */ private static ModuleNodeImpl getModuleByNameAndRevision(final Map<String, Map<Date, ModuleNodeImpl>> moduleGraph, final String fromName, final Date fromRevision, final String toName, final Date toRevision) { ModuleNodeImpl to = null; if (moduleGraph.get(toName) == null || !moduleGraph.get(toName).containsKey(toRevision)) { // If revision is not specified in import, but module exists // with different revisions, take first if (moduleGraph.get(toName) != null && !moduleGraph.get(toName).isEmpty() && toRevision.equals(DEFAULT_REVISION)) { to = moduleGraph.get(toName).values().iterator().next(); LOGGER.trace(String .format("Import:%s:%s by module:%s:%s does not specify revision, using:%s:%s for module dependency sort", toName, formatRevDate(toRevision), fromName, formatRevDate(fromRevision), to.getName(), formatRevDate(to.getRevision()))); } else { LOGGER.warn(String.format("Not existing module imported:%s:%s by:%s:%s", toName, formatRevDate(toRevision), fromName, formatRevDate(fromRevision))); LOGGER.warn("Available models: {}", moduleGraph); ex(String.format("Not existing module imported:%s:%s by:%s:%s", toName, formatRevDate(toRevision), fromName, formatRevDate(fromRevision))); } } else { to = moduleGraph.get(toName).get(toRevision); } return to; } private static void ex(final String message) { throw new YangValidationException(message); } /** * Extract dependencies from module builders or modules to fill dependency * graph */ private static void processModules(final Map<String, Map<Date, ModuleNodeImpl>> moduleGraph, final Iterable<Module> modules) { // Process nodes for (final Module momb : modules) { final String name = momb.getName(); Date rev = momb.getRevision(); if (rev == null) { rev = DEFAULT_REVISION; } if (moduleGraph.get(name) == null) { moduleGraph.put(name, Maps.newHashMap()); } if (moduleGraph.get(name).get(rev) != null) { ex(String.format("Module:%s with revision:%s declared twice", name, formatRevDate(rev))); } moduleGraph.get(name).put(rev, new ModuleNodeImpl(name, rev, momb)); } } private static String formatRevDate(final Date rev) { return rev.equals(DEFAULT_REVISION) ? "default" : SimpleDateFormatUtil.getRevisionFormat().format(rev); } @VisibleForTesting static class ModuleNodeImpl extends NodeImpl { private final String name; private final Date revision; private final Module originalObject; public ModuleNodeImpl(final String name, final Date revision, final Module module) { this.name = name; this.revision = revision; this.originalObject = module; } public String getName() { return name; } public Date getRevision() { return revision; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + Objects.hashCode(name); result = prime * result + Objects.hashCode(revision); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ModuleNodeImpl other = (ModuleNodeImpl) obj; if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } if (revision == null) { if (other.revision != null) { return false; } } else if (!revision.equals(other.revision)) { return false; } return true; } @Override public String toString() { return "Module [name=" + name + ", revision=" + formatRevDate(revision) + "]"; } public Module getReference() { return originalObject; } } }