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