Bump versions to 13.0.4-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.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;
27
28 final class CollisionDomain {
29     abstract sealed 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 sealed 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 strategy.childPackage();
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 primary && strategy.rootName().equals(primary.strategy.rootName());
126         }
127     }
128
129     private final class Prefix extends Primary {
130         Prefix(final Generator gen, final ClassNamingStrategy strategy) {
131             super(gen, strategy);
132         }
133     }
134
135     private abstract sealed class Secondary extends Member {
136         private final String classSuffix;
137         final @NonNull Member classPrimary;
138
139         Secondary(final Generator gen, final Member primary, final String classSuffix) {
140             super(gen);
141             classPrimary = requireNonNull(primary);
142             this.classSuffix = requireNonNull(classSuffix);
143             primary.addSecondary(this);
144         }
145
146         @Override
147         final String computeCurrentClass() {
148             return classPrimary.currentClass() + classSuffix;
149         }
150
151         @Override
152         final boolean signalConflict() {
153             return classPrimary.signalConflict();
154         }
155
156         final void primaryConflict() {
157             super.signalConflict();
158         }
159
160         @Override
161         final boolean equalRoot(final Member other) {
162             return other instanceof Secondary sec
163                 && classPrimary.equalRoot(sec.classPrimary) && classSuffix.equals(sec.classSuffix);
164         }
165     }
166
167     private final class LeafSecondary extends Secondary {
168         LeafSecondary(final Generator gen, final Member classPrimary, final String classSuffix) {
169             super(gen, classPrimary, classSuffix);
170         }
171
172         @Override
173         String computeCurrentPackage() {
174             // This should never happen
175             throw new UnsupportedOperationException();
176         }
177     }
178
179     private final class SuffixSecondary extends Secondary {
180         private final AbstractQName packageSuffix;
181
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);
186         }
187
188         @Override
189         String computeCurrentPackage() {
190             return classPrimary.currentPackage() + '.' + packageString(packageSuffix);
191         }
192     }
193
194     private final class AugmentSecondary extends Secondary {
195         private final SchemaNodeIdentifier packageSuffix;
196
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);
201         }
202
203         @Override
204         String computeCurrentPackage() {
205             final Iterator<QName> it = packageSuffix.getNodeIdentifiers().iterator();
206
207             final StringBuilder sb = new StringBuilder();
208             sb.append(packageString(it.next()));
209             while (it.hasNext()) {
210                 sb.append('.').append(packageString(it.next()));
211             }
212             return sb.toString();
213         }
214     }
215
216     private final AbstractCompositeGenerator<?, ?> gen;
217
218     private List<Member> members = List.of();
219     private boolean solved;
220
221     CollisionDomain(final AbstractCompositeGenerator<?, ?> gen) {
222         this.gen = requireNonNull(gen);
223     }
224
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);
228     }
229
230     @NonNull Member addPrimary(final Generator memberGen, final ClassNamingStrategy strategy) {
231         return addMember(new Primary(memberGen, strategy));
232     }
233
234     @NonNull Member addSecondary(final Generator memberGen, final Member primary, final String classSuffix) {
235         return addMember(new LeafSecondary(memberGen, primary, classSuffix));
236     }
237
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()));
241     }
242
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()));
246     }
247
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));
251     }
252
253     /*
254      * Naming child nodes is tricky.
255      *
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.
258      *
259      * Furthermore not all localNames are valid Java class/package identifiers, hence even non-equal localNames can
260      * conflict on their mapping.
261      *
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.
264      *
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.
267      */
268     boolean findSolution() {
269         if (solved) {
270             // Already solved, nothing to do
271             return false;
272         }
273         if (members.size() < 2) {
274             // Zero or one member: no conflict possible
275             solved = true;
276             return false;
277         }
278
279         boolean result = false;
280         do {
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);
285             }
286
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) {
294                     it.remove();
295                 }
296             }
297
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.
301             solved = true;
302             if (!toAssign.isEmpty()) {
303                 result = true;
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()) {
306                     int remaining = 0;
307                     for (Member member : conflicting) {
308                         if (!member.signalConflict()) {
309                             remaining++;
310                         }
311                     }
312                     checkState(remaining < 2, "Failed to solve %s due to naming conflict among %s", this, conflicting);
313                 }
314             }
315         } while (!solved);
316
317         return result;
318     }
319
320     @Override
321     public String toString() {
322         return MoreObjects.toStringHelper(this).add("gen", gen).toString();
323     }
324
325     private @NonNull Member addMember(final @NonNull Member member) {
326         if (members.isEmpty()) {
327             members = new ArrayList<>();
328         }
329         members.add(member);
330         return member;
331     }
332
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('-', '.');
336     }
337 }