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