Adopt odlparent-10.0.0/yangtools-8.0.0-SNAPSHOT
[mdsal.git] / binding / mdsal-binding-generator / src / main / java / org / opendaylight / mdsal / binding / generator / impl / reactor / CollisionDomain.java
1 /*
2  * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.mdsal.binding.generator.impl.reactor;
9
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
12
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.mdsal.binding.spec.naming.BindingMapping;
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;
27
28 final class CollisionDomain {
29     abstract class Member {
30         private final Generator gen;
31
32         private List<Secondary> secondaries = List.of();
33         private String currentPackage;
34         private String currentClass;
35
36         Member(final Generator gen) {
37             this.gen = requireNonNull(gen);
38         }
39
40         final void addSecondary(final Secondary secondary) {
41             if (secondaries.isEmpty()) {
42                 secondaries = new ArrayList<>();
43             }
44             secondaries.add(requireNonNull(secondary));
45         }
46
47         final @NonNull String currentClass() {
48             if (currentClass == null) {
49                 currentClass = computeCurrentClass();
50             }
51             return currentClass;
52         }
53
54         final @NonNull String currentPackage() {
55             if (currentPackage == null) {
56                 currentPackage = computeCurrentPackage();
57             }
58             return currentPackage;
59         }
60
61         abstract boolean equalRoot(@NonNull Member other);
62
63         abstract String computeCurrentClass();
64
65         abstract String computeCurrentPackage();
66
67         boolean signalConflict() {
68             solved = false;
69             currentClass = null;
70             currentPackage = null;
71
72             for (Secondary secondary : secondaries) {
73                 secondary.primaryConflict();
74             }
75
76             return true;
77         }
78
79         @Override
80         public final String toString() {
81             return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
82         }
83
84         ToStringHelper addToStringAttributes(final ToStringHelper helper) {
85             return helper.add("gen", gen).add("class", currentClass).add("package", currentPackage);
86         }
87     }
88
89     private class Primary extends Member {
90         private ClassNamingStrategy strategy;
91
92         Primary(final Generator gen, final ClassNamingStrategy strategy) {
93             super(gen);
94             this.strategy = requireNonNull(strategy);
95         }
96
97         @Override
98         final String computeCurrentClass() {
99             return strategy.simpleClassName();
100         }
101
102         @Override
103         final String computeCurrentPackage() {
104             return packageString(strategy.nodeIdentifier());
105         }
106
107         @Override
108         final boolean signalConflict() {
109             final ClassNamingStrategy newStrategy = strategy.fallback();
110             if (newStrategy == null) {
111                 return false;
112             }
113
114             strategy = newStrategy;
115             return super.signalConflict();
116         }
117
118         @Override
119         final ToStringHelper addToStringAttributes(final ToStringHelper helper) {
120             return super.addToStringAttributes(helper.add("strategy", strategy));
121         }
122
123         @Override
124         boolean equalRoot(final Member other) {
125             return other instanceof Primary && strategy.nodeIdentifier().getLocalName().equals(
126                 ((Primary) other).strategy.nodeIdentifier().getLocalName());
127         }
128     }
129
130     private final class Prefix extends Primary {
131         Prefix(final Generator gen, final ClassNamingStrategy strategy) {
132             super(gen, strategy);
133         }
134     }
135
136     private abstract class Secondary extends Member {
137         private final String classSuffix;
138         final Member classPrimary;
139
140         Secondary(final Generator gen, final Member primary, final String classSuffix) {
141             super(gen);
142             classPrimary = requireNonNull(primary);
143             this.classSuffix = requireNonNull(classSuffix);
144             primary.addSecondary(this);
145         }
146
147         @Override
148         final String computeCurrentClass() {
149             return classPrimary.currentClass() + classSuffix;
150         }
151
152         @Override
153         final boolean signalConflict() {
154             return classPrimary.signalConflict();
155         }
156
157         final void primaryConflict() {
158             super.signalConflict();
159         }
160
161         @Override
162         final boolean equalRoot(final Member other) {
163             if (other instanceof Secondary) {
164                 final Secondary sec = (Secondary) other;
165                 return classPrimary.equalRoot(sec.classPrimary) && classSuffix.equals(sec.classSuffix);
166             }
167             return false;
168         }
169     }
170
171     private final class LeafSecondary extends Secondary {
172         LeafSecondary(final Generator gen, final Member classPrimary, final String classSuffix) {
173             super(gen, classPrimary, classSuffix);
174         }
175
176         @Override
177         String computeCurrentPackage() {
178             // This should never happen
179             throw new UnsupportedOperationException();
180         }
181     }
182
183     private final class SuffixSecondary extends Secondary {
184         private final AbstractQName packageSuffix;
185
186         SuffixSecondary(final Generator gen, final Member primaryClass, final String classSuffix,
187                 final AbstractQName packageSuffix) {
188             super(gen, primaryClass, classSuffix);
189             this.packageSuffix = requireNonNull(packageSuffix);
190         }
191
192         @Override
193         String computeCurrentPackage() {
194             return classPrimary.currentPackage() + '.' + packageString(packageSuffix);
195         }
196     }
197
198     private final class AugmentSecondary extends Secondary {
199         private final SchemaNodeIdentifier packageSuffix;
200
201         AugmentSecondary(final AbstractAugmentGenerator gen, final Member primary, final String classSuffix,
202                 final SchemaNodeIdentifier packageSuffix) {
203             super(gen, primary, classSuffix);
204             this.packageSuffix = requireNonNull(packageSuffix);
205         }
206
207         @Override
208         String computeCurrentPackage() {
209             final Iterator<QName> it = packageSuffix.getNodeIdentifiers().iterator();
210
211             final StringBuilder sb = new StringBuilder();
212             sb.append(packageString(it.next()));
213             while (it.hasNext()) {
214                 sb.append('.').append(packageString(it.next()));
215             }
216             return sb.toString();
217         }
218     }
219
220     private final AbstractCompositeGenerator<?, ?> gen;
221
222     private List<Member> members = List.of();
223     private boolean solved;
224
225     CollisionDomain(final AbstractCompositeGenerator<?, ?> gen) {
226         this.gen = requireNonNull(gen);
227     }
228
229     @NonNull Member addPrefix(final Generator memberGen, final ClassNamingStrategy strategy) {
230         // Note that contrary to the method name, we are not adding the result to members
231         return new Prefix(memberGen, strategy);
232     }
233
234     @NonNull Member addPrimary(final Generator memberGen, final ClassNamingStrategy strategy) {
235         return addMember(new Primary(memberGen, strategy));
236     }
237
238     @NonNull Member addSecondary(final Generator memberGen, final Member primary, final String classSuffix) {
239         return addMember(new LeafSecondary(memberGen, primary, classSuffix));
240     }
241
242     @NonNull Member addSecondary(final RpcInputGenerator memberGen, final Member primary) {
243         return addMember(new SuffixSecondary(memberGen, primary, BindingMapping.RPC_INPUT_SUFFIX,
244             memberGen.statement().argument()));
245     }
246
247     @NonNull Member addSecondary(final RpcOutputGenerator memberGen, final Member primary) {
248         return addMember(new SuffixSecondary(memberGen, primary, BindingMapping.RPC_OUTPUT_SUFFIX,
249             memberGen.statement().argument()));
250     }
251
252     @NonNull Member addSecondary(final AbstractAugmentGenerator memberGen, final Member classPrimary,
253             final String classSuffix, final SchemaNodeIdentifier packageSuffix) {
254         return addMember(new AugmentSecondary(memberGen, classPrimary, classSuffix, packageSuffix));
255     }
256
257     /*
258      * Naming child nodes is tricky.
259      *
260      * We map multiple YANG namespaces (see YangStatementNamespace) onto a single Java namespace
261      * (package/class names), hence we can have legal conflicts on same localName.
262      *
263      * Furthermore not all localNames are valid Java class/package identifiers, hence even non-equal localNames can
264      * conflict on their mapping.
265      *
266      * Final complication is that we allow user to control preferred name, or we generate one, and we try to come up
267      * with nice names like 'foo-bar' becoming FooBar and similar.
268      *
269      * In all cases we want to end up with cutest possible names while also never creating duplicates. For that we
270      * start with each child telling us their preferred name and we collect name->child mapping.
271      */
272     boolean findSolution() {
273         if (solved) {
274             // Already solved, nothing to do
275             return false;
276         }
277         if (members.size() < 2) {
278             // Zero or one member: no conflict possible
279             solved = true;
280             return false;
281         }
282
283         boolean result = false;
284         do {
285             // Construct mapping to discover any naming overlaps.
286             final Multimap<String, Member> toAssign = ArrayListMultimap.create();
287             for (Member member : members) {
288                 toAssign.put(member.currentClass(), member);
289             }
290
291             // Deal with names which do not create a conflict. This is very simple and also very effective, we rarely
292             // run into conflicts.
293             final var it = toAssign.asMap().entrySet().iterator();
294             while (it.hasNext()) {
295                 final Entry<String, Collection<Member>> entry = it.next();
296                 final Collection<Member> assignees = entry.getValue();
297                 if (assignees.size() == 1) {
298                     it.remove();
299                 }
300             }
301
302             // This looks counter-intuitive, but the idea is simple: the act of assigning a different strategy may end
303             // up creating conflicts where there were none -- including in this domain. Marking this bit allows us to
304             // react to such invalidation chains and retry the process.
305             solved = true;
306             if (!toAssign.isEmpty()) {
307                 result = true;
308                 // We still have some assignments we need to resolve -- which means we need to change their strategy.
309                 for (Collection<Member> conflicting : toAssign.asMap().values()) {
310                     int remaining = 0;
311                     for (Member member : conflicting) {
312                         if (!member.signalConflict()) {
313                             remaining++;
314                         }
315                     }
316                     checkState(remaining < 2, "Failed to solve %s due to naming conflict among %s", this, conflicting);
317                 }
318             }
319         } while (!solved);
320
321         return result;
322     }
323
324     @Override
325     public String toString() {
326         return MoreObjects.toStringHelper(this).add("gen", gen).toString();
327     }
328
329     private @NonNull Member addMember(final @NonNull Member member) {
330         if (members.isEmpty()) {
331             members = new ArrayList<>();
332         }
333         members.add(member);
334         return member;
335     }
336
337     private static @NonNull String packageString(final AbstractQName component) {
338         // Replace dashes with dots, as dashes are not allowed in package names
339         return component.getLocalName().replace('-', '.');
340     }
341 }