/* * Copyright (c) 2015 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.stmt.reactor; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.SetMultimap; import com.google.common.collect.Table; import com.google.common.collect.TreeBasedTable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.SortedMap; import org.opendaylight.yangtools.yang.common.Empty; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.QNameModule; import org.opendaylight.yangtools.yang.common.Revision; import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified; import org.opendaylight.yangtools.yang.common.YangVersion; import org.opendaylight.yangtools.yang.model.api.meta.DeclaredStatement; import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement; import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier; import org.opendaylight.yangtools.yang.model.api.stmt.FeatureSet; import org.opendaylight.yangtools.yang.parser.spi.ParserNamespaces; import org.opendaylight.yangtools.yang.parser.spi.meta.ModelProcessingPhase; import org.opendaylight.yangtools.yang.parser.spi.meta.MutableStatement; import org.opendaylight.yangtools.yang.parser.spi.meta.NamespaceNotAvailableException; import org.opendaylight.yangtools.yang.parser.spi.meta.NamespaceStorage.GlobalStorage; import org.opendaylight.yangtools.yang.parser.spi.meta.ParserNamespace; import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; import org.opendaylight.yangtools.yang.parser.spi.meta.SomeModifiersUnresolvedException; import org.opendaylight.yangtools.yang.parser.spi.meta.StatementSupportBundle; import org.opendaylight.yangtools.yang.parser.spi.source.SourceException; import org.opendaylight.yangtools.yang.parser.spi.source.StatementStreamSource; import org.opendaylight.yangtools.yang.parser.spi.validation.ValidationBundles; import org.opendaylight.yangtools.yang.parser.spi.validation.ValidationBundles.ValidationBundleType; import org.opendaylight.yangtools.yang.parser.stmt.reactor.SourceSpecificContext.PhaseCompletionProgress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; final class BuildGlobalContext extends AbstractNamespaceStorage implements GlobalStorage { private static final Logger LOG = LoggerFactory.getLogger(BuildGlobalContext.class); private static final ModelProcessingPhase[] PHASE_EXECUTION_ORDER = { ModelProcessingPhase.SOURCE_PRE_LINKAGE, ModelProcessingPhase.SOURCE_LINKAGE, ModelProcessingPhase.STATEMENT_DEFINITION, ModelProcessingPhase.FULL_DECLARATION, ModelProcessingPhase.EFFECTIVE_MODEL }; private final Table> definitions = HashBasedTable.create(); private final Map> modelDefinedStmtDefs = new HashMap<>(); private final Map, BehaviourNamespaceAccess> supportedNamespaces = new HashMap<>(); private final List mutableStatementsToSeal = new ArrayList<>(); private final ImmutableMap supports; private final Set sources = new HashSet<>(); private final ImmutableSet supportedVersions; private Set libSources = new HashSet<>(); private ModelProcessingPhase currentPhase = ModelProcessingPhase.INIT; private ModelProcessingPhase finishedPhase = ModelProcessingPhase.INIT; BuildGlobalContext(final ImmutableMap supports, final ImmutableMap> supportedValidation) { this.supports = requireNonNull(supports, "BuildGlobalContext#supports cannot be null"); final var access = accessNamespace(ValidationBundles.NAMESPACE); for (var validationBundle : supportedValidation.entrySet()) { access.valueTo(this, validationBundle.getKey(), validationBundle.getValue()); } supportedVersions = ImmutableSet.copyOf( verifyNotNull(supports.get(ModelProcessingPhase.INIT)).getSupportedVersions()); } StatementSupportBundle getSupportsForPhase(final ModelProcessingPhase phase) { return supports.get(phase); } void addSource(final StatementStreamSource source) { sources.add(new SourceSpecificContext(this, source)); } void addLibSource(final StatementStreamSource libSource) { checkState(currentPhase == ModelProcessingPhase.INIT, "Add library source is allowed in ModelProcessingPhase.INIT only"); libSources.add(new SourceSpecificContext(this, libSource)); } void setSupportedFeatures(final FeatureSet supportedFeatures) { addToNamespace(ParserNamespaces.SUPPORTED_FEATURES, Empty.value(), requireNonNull(supportedFeatures)); } void setModulesDeviatedByModules(final SetMultimap modulesDeviatedByModules) { addToNamespace(ParserNamespaces.MODULES_DEVIATED_BY, Empty.value(), ImmutableSetMultimap.copyOf(modulesDeviatedByModules)); } @Override BehaviourNamespaceAccess accessNamespace(final ParserNamespace namespace) { @SuppressWarnings("unchecked") final var existing = (BehaviourNamespaceAccess) supportedNamespaces.get(namespace); if (existing != null) { return existing; } final var behaviour = verifyNotNull(supports.get(currentPhase), "No support for phase %s", currentPhase) .namespaceBehaviourOf(namespace); if (behaviour == null) { throw new NamespaceNotAvailableException( "Namespace " + namespace + " is not available in phase " + currentPhase); } final var created = new BehaviourNamespaceAccess<>(this, behaviour); supportedNamespaces.put(namespace, created); return created; } StatementDefinitionContext getStatementDefinition(final YangVersion version, final QName name) { StatementDefinitionContext potential = definitions.get(version, name); if (potential == null) { final var potentialRaw = verifyNotNull(supports.get(currentPhase)).getStatementDefinition(version, name); if (potentialRaw != null) { potential = new StatementDefinitionContext<>(potentialRaw); definitions.put(version, name, potential); } } return potential; } StatementDefinitionContext getModelDefinedStatementDefinition(final QName name) { return modelDefinedStmtDefs.get(name); } void putModelDefinedStatementDefinition(final QName name, final StatementDefinitionContext def) { modelDefinedStmtDefs.put(name, def); } private void executePhases() throws ReactorException { for (final ModelProcessingPhase phase : PHASE_EXECUTION_ORDER) { startPhase(phase); loadPhaseStatements(); completePhaseActions(); endPhase(phase); } } ReactorDeclaredModel build() throws ReactorException { executePhases(); return transform(); } EffectiveSchemaContext buildEffective() throws ReactorException { executePhases(); return transformEffective(); } private ReactorDeclaredModel transform() { checkState(finishedPhase == ModelProcessingPhase.EFFECTIVE_MODEL); final var rootStatements = new ArrayList>(sources.size()); for (var source : sources) { rootStatements.add(source.declaredRoot()); } return new ReactorDeclaredModel(rootStatements); } private SomeModifiersUnresolvedException propagateException(final SourceSpecificContext source, final RuntimeException cause) throws SomeModifiersUnresolvedException { final SourceIdentifier sourceId = source.identifySource(); if (!(cause instanceof SourceException)) { /* * This should not be happening as all our processing should provide SourceExceptions. * We will wrap the exception to provide enough information to identify the problematic model, * but also emit a warning so the offending codepath will get fixed. */ LOG.warn("Unexpected error processing source {}. Please file an issue with this model attached.", sourceId, cause); } throw new SomeModifiersUnresolvedException(currentPhase, sourceId, cause); } @SuppressWarnings("checkstyle:illegalCatch") private EffectiveSchemaContext transformEffective() throws ReactorException { checkState(finishedPhase == ModelProcessingPhase.EFFECTIVE_MODEL); final var rootStatements = new ArrayList>(sources.size()); final var rootEffectiveStatements = new ArrayList>(sources.size()); for (var source : sources) { try { rootStatements.add(source.declaredRoot()); rootEffectiveStatements.add(source.effectiveRoot()); } catch (final RuntimeException ex) { throw propagateException(source, ex); } } sealMutableStatements(); return EffectiveSchemaContext.create(rootStatements, rootEffectiveStatements); } private void startPhase(final ModelProcessingPhase phase) { checkState(Objects.equals(finishedPhase, phase.getPreviousPhase())); startPhaseFor(phase, sources); startPhaseFor(phase, libSources); currentPhase = phase; LOG.debug("Global phase {} started", phase); } private static void startPhaseFor(final ModelProcessingPhase phase, final Set sources) { for (final SourceSpecificContext source : sources) { source.startPhase(phase); } } private void loadPhaseStatements() throws ReactorException { checkState(currentPhase != null); loadPhaseStatementsFor(sources); loadPhaseStatementsFor(libSources); } @SuppressWarnings("checkstyle:illegalCatch") private void loadPhaseStatementsFor(final Set srcs) throws ReactorException { for (final SourceSpecificContext source : srcs) { try { source.loadStatements(); } catch (final RuntimeException ex) { throw propagateException(source, ex); } } } private SomeModifiersUnresolvedException addSourceExceptions(final List sourcesToProgress) { boolean addedCause = false; SomeModifiersUnresolvedException buildFailure = null; for (var failedSource : sourcesToProgress) { final var optSourceEx = failedSource.failModifiers(currentPhase); if (optSourceEx.isEmpty()) { continue; } final var sourceEx = optSourceEx.orElseThrow(); // Workaround for broken logging implementations which ignore // suppressed exceptions final var cause = sourceEx.getCause() != null ? sourceEx.getCause() : sourceEx; if (LOG.isDebugEnabled()) { LOG.error("Failed to parse YANG from source {}", failedSource, sourceEx); } else { LOG.error("Failed to parse YANG from source {}: {}", failedSource, cause.getMessage()); } final var suppressed = sourceEx.getSuppressed(); if (suppressed.length > 0) { LOG.error("{} additional errors reported:", suppressed.length); int count = 1; for (var supp : suppressed) { LOG.error("Error {}: {}", count, supp.getMessage()); count++; } } if (!addedCause) { addedCause = true; final var sourceId = failedSource.identifySource(); buildFailure = new SomeModifiersUnresolvedException(currentPhase, sourceId, sourceEx); } else { buildFailure.addSuppressed(sourceEx); } } return buildFailure; } @SuppressWarnings("checkstyle:illegalCatch") private void completePhaseActions() throws ReactorException { checkState(currentPhase != null); final List sourcesToProgress = new ArrayList<>(sources); if (!libSources.isEmpty()) { checkState(currentPhase == ModelProcessingPhase.SOURCE_PRE_LINKAGE, "Yang library sources should be empty after ModelProcessingPhase.SOURCE_PRE_LINKAGE, " + "but current phase was %s", currentPhase); sourcesToProgress.addAll(libSources); } boolean progressing = true; while (progressing) { // We reset progressing to false. progressing = false; final Iterator currentSource = sourcesToProgress.iterator(); while (currentSource.hasNext()) { final SourceSpecificContext nextSourceCtx = currentSource.next(); try { final PhaseCompletionProgress sourceProgress = nextSourceCtx.tryToCompletePhase(currentPhase.executionOrder()); switch (sourceProgress) { case FINISHED: currentSource.remove(); // we were able to make progress in computation progressing = true; break; case PROGRESS: progressing = true; break; case NO_PROGRESS: // Noop break; default: throw new IllegalStateException("Unsupported phase progress " + sourceProgress); } } catch (final RuntimeException ex) { throw propagateException(nextSourceCtx, ex); } } } if (!libSources.isEmpty()) { final Set requiredLibs = getRequiredSourcesFromLib(); sources.addAll(requiredLibs); libSources = ImmutableSet.of(); /* * We want to report errors of relevant sources only, so any others can * be removed. */ sourcesToProgress.retainAll(sources); } if (!sourcesToProgress.isEmpty()) { final SomeModifiersUnresolvedException buildFailure = addSourceExceptions(sourcesToProgress); if (buildFailure != null) { throw buildFailure; } } } private Set getRequiredSourcesFromLib() { checkState(currentPhase == ModelProcessingPhase.SOURCE_PRE_LINKAGE, "Required library sources can be collected only in ModelProcessingPhase.SOURCE_PRE_LINKAGE phase," + " but current phase was %s", currentPhase); final TreeBasedTable, SourceSpecificContext> libSourcesTable = TreeBasedTable.create(Unqualified::compareTo, Revision::compare); for (final SourceSpecificContext libSource : libSources) { final SourceIdentifier libSourceIdentifier = requireNonNull(libSource.getRootIdentifier()); libSourcesTable.put(libSourceIdentifier.name(), Optional.ofNullable(libSourceIdentifier.revision()), libSource); } final Set requiredLibs = new HashSet<>(); for (final SourceSpecificContext source : sources) { collectRequiredSourcesFromLib(libSourcesTable, requiredLibs, source); removeConflictingLibSources(source, requiredLibs); } return requiredLibs; } private void collectRequiredSourcesFromLib( final TreeBasedTable, SourceSpecificContext> libSourcesTable, final Set requiredLibs, final SourceSpecificContext source) { for (final SourceIdentifier requiredSource : source.getRequiredSources()) { final SourceSpecificContext libSource = getRequiredLibSource(requiredSource, libSourcesTable); if (libSource != null && requiredLibs.add(libSource)) { collectRequiredSourcesFromLib(libSourcesTable, requiredLibs, libSource); } } } private static SourceSpecificContext getRequiredLibSource(final SourceIdentifier requiredSource, final TreeBasedTable, SourceSpecificContext> libSourcesTable) { final var revision = requiredSource.revision(); return revision != null ? libSourcesTable.get(requiredSource.name(), Optional.of(revision)) : getLatestRevision(libSourcesTable.row(requiredSource.name())); } private static SourceSpecificContext getLatestRevision( final SortedMap, SourceSpecificContext> sourceMap) { return sourceMap != null && !sourceMap.isEmpty() ? sourceMap.get(sourceMap.lastKey()) : null; } // removes required library sources which would cause namespace/name conflict with one of the main sources // later in the parsing process. this can happen if we add a parent module or a submodule as a main source // and the same parent module or submodule is added as one of the library sources. // such situation may occur when using the yang-system-test artifact - if a parent module/submodule is specified // as its argument and the same dir is specified as one of the library dirs through -p option). private static void removeConflictingLibSources(final SourceSpecificContext source, final Set requiredLibs) { final Iterator requiredLibsIter = requiredLibs.iterator(); while (requiredLibsIter.hasNext()) { final SourceSpecificContext currentReqSource = requiredLibsIter.next(); if (source.getRootIdentifier().equals(currentReqSource.getRootIdentifier())) { requiredLibsIter.remove(); } } } private void endPhase(final ModelProcessingPhase phase) { checkState(currentPhase == phase); finishedPhase = currentPhase; LOG.debug("Global phase {} finished", phase); } Set getSources() { return sources; } public Set getSupportedVersions() { return supportedVersions; } void addMutableStmtToSeal(final MutableStatement mutableStatement) { mutableStatementsToSeal.add(mutableStatement); } void sealMutableStatements() { for (final MutableStatement mutableStatement : mutableStatementsToSeal) { mutableStatement.seal(); } mutableStatementsToSeal.clear(); } }