2 * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.mdsal.binding.generator.impl.reactor;
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.base.MoreObjects;
14 import com.google.common.base.MoreObjects.ToStringHelper;
15 import com.google.common.collect.ArrayListMultimap;
16 import com.google.common.collect.Multimap;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Iterator;
20 import java.util.List;
21 import java.util.Map.Entry;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.opendaylight.yangtools.yang.binding.contract.Naming;
24 import org.opendaylight.yangtools.yang.common.AbstractQName;
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
28 final class CollisionDomain {
29 abstract sealed class Member {
30 private final Generator gen;
32 private List<Secondary> secondaries = List.of();
33 private String currentPackage;
34 private String currentClass;
36 Member(final Generator gen) {
37 this.gen = requireNonNull(gen);
40 final void addSecondary(final Secondary secondary) {
41 if (secondaries.isEmpty()) {
42 secondaries = new ArrayList<>();
44 secondaries.add(requireNonNull(secondary));
47 final @NonNull String currentClass() {
48 if (currentClass == null) {
49 currentClass = computeCurrentClass();
54 final @NonNull String currentPackage() {
55 if (currentPackage == null) {
56 currentPackage = computeCurrentPackage();
58 return currentPackage;
61 abstract boolean equalRoot(@NonNull Member other);
63 abstract String computeCurrentClass();
65 abstract String computeCurrentPackage();
67 boolean signalConflict() {
70 currentPackage = null;
72 for (Secondary secondary : secondaries) {
73 secondary.primaryConflict();
80 public final String toString() {
81 return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
84 ToStringHelper addToStringAttributes(final ToStringHelper helper) {
85 return helper.add("gen", gen).add("class", currentClass).add("package", currentPackage);
89 private sealed class Primary extends Member {
90 private ClassNamingStrategy strategy;
92 Primary(final Generator gen, final ClassNamingStrategy strategy) {
94 this.strategy = requireNonNull(strategy);
98 final String computeCurrentClass() {
99 return strategy.simpleClassName();
103 final String computeCurrentPackage() {
104 return strategy.childPackage();
108 final boolean signalConflict() {
109 final ClassNamingStrategy newStrategy = strategy.fallback();
110 if (newStrategy == null) {
114 strategy = newStrategy;
115 return super.signalConflict();
119 final ToStringHelper addToStringAttributes(final ToStringHelper helper) {
120 return super.addToStringAttributes(helper.add("strategy", strategy));
124 boolean equalRoot(final Member other) {
125 return other instanceof Primary primary && strategy.rootName().equals(primary.strategy.rootName());
129 private final class Prefix extends Primary {
130 Prefix(final Generator gen, final ClassNamingStrategy strategy) {
131 super(gen, strategy);
135 private abstract sealed class Secondary extends Member {
136 private final String classSuffix;
137 final @NonNull Member classPrimary;
139 Secondary(final Generator gen, final Member primary, final String classSuffix) {
141 classPrimary = requireNonNull(primary);
142 this.classSuffix = requireNonNull(classSuffix);
143 primary.addSecondary(this);
147 final String computeCurrentClass() {
148 return classPrimary.currentClass() + classSuffix;
152 final boolean signalConflict() {
153 return classPrimary.signalConflict();
156 final void primaryConflict() {
157 super.signalConflict();
161 final boolean equalRoot(final Member other) {
162 return other instanceof Secondary sec
163 && classPrimary.equalRoot(sec.classPrimary) && classSuffix.equals(sec.classSuffix);
167 private final class LeafSecondary extends Secondary {
168 LeafSecondary(final Generator gen, final Member classPrimary, final String classSuffix) {
169 super(gen, classPrimary, classSuffix);
173 String computeCurrentPackage() {
174 // This should never happen
175 throw new UnsupportedOperationException();
179 private final class SuffixSecondary extends Secondary {
180 private final AbstractQName packageSuffix;
182 SuffixSecondary(final Generator gen, final Member primaryClass, final String classSuffix,
183 final AbstractQName packageSuffix) {
184 super(gen, primaryClass, classSuffix);
185 this.packageSuffix = requireNonNull(packageSuffix);
189 String computeCurrentPackage() {
190 return classPrimary.currentPackage() + '.' + packageString(packageSuffix);
194 private final class AugmentSecondary extends Secondary {
195 private final SchemaNodeIdentifier packageSuffix;
197 AugmentSecondary(final AbstractAugmentGenerator gen, final Member primary, final String classSuffix,
198 final SchemaNodeIdentifier packageSuffix) {
199 super(gen, primary, classSuffix);
200 this.packageSuffix = requireNonNull(packageSuffix);
204 String computeCurrentPackage() {
205 final Iterator<QName> it = packageSuffix.getNodeIdentifiers().iterator();
207 final StringBuilder sb = new StringBuilder();
208 sb.append(packageString(it.next()));
209 while (it.hasNext()) {
210 sb.append('.').append(packageString(it.next()));
212 return sb.toString();
216 private final AbstractCompositeGenerator<?, ?> gen;
218 private List<Member> members = List.of();
219 private boolean solved;
221 CollisionDomain(final AbstractCompositeGenerator<?, ?> gen) {
222 this.gen = requireNonNull(gen);
225 @NonNull Member addPrefix(final Generator memberGen, final ClassNamingStrategy strategy) {
226 // Note that contrary to the method name, we are not adding the result to members
227 return new Prefix(memberGen, strategy);
230 @NonNull Member addPrimary(final Generator memberGen, final ClassNamingStrategy strategy) {
231 return addMember(new Primary(memberGen, strategy));
234 @NonNull Member addSecondary(final Generator memberGen, final Member primary, final String classSuffix) {
235 return addMember(new LeafSecondary(memberGen, primary, classSuffix));
238 @NonNull Member addSecondary(final InputGenerator memberGen, final Member primary) {
239 return addMember(new SuffixSecondary(memberGen, primary, Naming.RPC_INPUT_SUFFIX,
240 memberGen.statement().argument()));
243 @NonNull Member addSecondary(final OutputGenerator memberGen, final Member primary) {
244 return addMember(new SuffixSecondary(memberGen, primary, Naming.RPC_OUTPUT_SUFFIX,
245 memberGen.statement().argument()));
248 @NonNull Member addSecondary(final AbstractAugmentGenerator memberGen, final Member classPrimary,
249 final String classSuffix, final SchemaNodeIdentifier packageSuffix) {
250 return addMember(new AugmentSecondary(memberGen, classPrimary, classSuffix, packageSuffix));
254 * Naming child nodes is tricky.
256 * We map multiple YANG namespaces (see YangStatementNamespace) onto a single Java namespace
257 * (package/class names), hence we can have legal conflicts on same localName.
259 * Furthermore not all localNames are valid Java class/package identifiers, hence even non-equal localNames can
260 * conflict on their mapping.
262 * Final complication is that we allow user to control preferred name, or we generate one, and we try to come up
263 * with nice names like 'foo-bar' becoming FooBar and similar.
265 * In all cases we want to end up with cutest possible names while also never creating duplicates. For that we
266 * start with each child telling us their preferred name and we collect name->child mapping.
268 boolean findSolution() {
270 // Already solved, nothing to do
273 if (members.size() < 2) {
274 // Zero or one member: no conflict possible
279 boolean result = false;
281 // Construct mapping to discover any naming overlaps.
282 final Multimap<String, Member> toAssign = ArrayListMultimap.create();
283 for (Member member : members) {
284 toAssign.put(member.currentClass(), member);
287 // Deal with names which do not create a conflict. This is very simple and also very effective, we rarely
288 // run into conflicts.
289 final var it = toAssign.asMap().entrySet().iterator();
290 while (it.hasNext()) {
291 final Entry<String, Collection<Member>> entry = it.next();
292 final Collection<Member> assignees = entry.getValue();
293 if (assignees.size() == 1) {
298 // This looks counter-intuitive, but the idea is simple: the act of assigning a different strategy may end
299 // up creating conflicts where there were none -- including in this domain. Marking this bit allows us to
300 // react to such invalidation chains and retry the process.
302 if (!toAssign.isEmpty()) {
304 // We still have some assignments we need to resolve -- which means we need to change their strategy.
305 for (Collection<Member> conflicting : toAssign.asMap().values()) {
307 for (Member member : conflicting) {
308 if (!member.signalConflict()) {
312 checkState(remaining < 2, "Failed to solve %s due to naming conflict among %s", this, conflicting);
321 public String toString() {
322 return MoreObjects.toStringHelper(this).add("gen", gen).toString();
325 private @NonNull Member addMember(final @NonNull Member member) {
326 if (members.isEmpty()) {
327 members = new ArrayList<>();
333 // Replace dashes with dots, as dashes are not allowed in package names
334 static @NonNull String packageString(final AbstractQName component) {
335 return component.getLocalName().replace('-', '.');