Skip to content

Commit

Permalink
fix: @SchemaSwap can only be used once
Browse files Browse the repository at this point in the history
@SchemaSwap is now repeatable, so multiple modifications can be applied from the same resource root.
Fixed a bug when the swap was "used up" after just one usage, meaning that if the referenced type was used multiple times in object hierarchy, only the first one was swapped.

Fixes fabric8io#4350
  • Loading branch information
xRodney committed Aug 26, 2022
1 parent 16f4521 commit a1dd23a
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@
* Fix #4256: crd-generator-apt pom.xml includes transitive dependencies
* Fix #4294: crd-generator respects JsonIgnore annotations on enum properties
* Fix #4320: corrected leader transitions field on leader election leases
* Fix #4350: SchemaSwap annotation is now repeatable and is applied multiple times if classes are used more than once in the class hierarchy


#### Improvements
Expand Down
Expand Up @@ -81,6 +81,7 @@ public abstract class AbstractJsonSchema<T, B> {
public static final String ANNOTATION_NOT_NULL = "javax.validation.constraints.NotNull";
public static final String ANNOTATION_SCHEMA_FROM = "io.fabric8.crd.generator.annotation.SchemaFrom";
public static final String ANNOTATION_SCHEMA_SWAP = "io.fabric8.crd.generator.annotation.SchemaSwap";
public static final String ANNOTATION_SCHEMA_SWAPS = "io.fabric8.crd.generator.annotation.SchemaSwaps";

public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";

Expand Down Expand Up @@ -119,9 +120,9 @@ public static String getSchemaTypeFor(TypeRef typeRef) {
* @return The schema.
*/
protected T internalFrom(TypeDef definition, String... ignore) {
List<InternalSchemaSwap> schemaSwaps = new ArrayList<>();
InternalSchemaSwaps schemaSwaps = new InternalSchemaSwaps();
T ret = internalFromImpl(definition, new HashSet<>(), schemaSwaps, ignore);
validateRemainingSchemaSwaps("unmatched class", schemaSwaps);
validateRemainingSchemaSwaps(schemaSwaps);
return ret;
}

Expand Down Expand Up @@ -187,25 +188,53 @@ private static ClassRef extractClassRef(Object type) {
}
}

private InternalSchemaSwap extractSchemaSwap(AnnotationRef annotation) {
Map<String, Object> params = annotation.getParameters();
return new InternalSchemaSwap(
extractClassRef(params.get("originalType")),
(String) params.get("fieldName"),
extractClassRef(params.get("targetType")));
private void extractSchemaSwaps(ClassRef definitionType, AnnotationRef annotation, InternalSchemaSwaps schemaSwaps) {
String fullyQualifiedName = annotation.getClassRef().getFullyQualifiedName();
switch (fullyQualifiedName) {
case ANNOTATION_SCHEMA_SWAP:
extractSchemaSwap(definitionType, annotation, schemaSwaps);
break;
case ANNOTATION_SCHEMA_SWAPS:
Map<String, Object> params = annotation.getParameters();
Object[] values = (Object[]) params.get("value");
for (Object value : values) {
extractSchemaSwap(definitionType, value, schemaSwaps);
}
break;
}
}

private void extractSchemaSwap(ClassRef definitionType, Object annotation, InternalSchemaSwaps schemaSwaps) {
if (annotation instanceof SchemaSwap) {
SchemaSwap schemaSwap = (SchemaSwap) annotation;
schemaSwaps.registerSwap(definitionType,
extractClassRef(schemaSwap.originalType()),
schemaSwap.fieldName(),
extractClassRef(schemaSwap.targetType()));

} else if (annotation instanceof AnnotationRef
&& ((AnnotationRef) annotation).getClassRef().getFullyQualifiedName().equals(ANNOTATION_SCHEMA_SWAP)) {
Map<String, Object> params = ((AnnotationRef) annotation).getParameters();
schemaSwaps.registerSwap(definitionType,
extractClassRef(params.get("originalType")),
(String) params.get("fieldName"),
extractClassRef(params.get("targetType")));

} else {
throw new IllegalArgumentException("Unmanaged annotation type passed to the SchemaSwaps: " + annotation);
}
}

private void validateRemainingSchemaSwaps(String error, List<InternalSchemaSwap> schemaSwaps) {
if (!schemaSwaps.isEmpty()) {
String umatchedSchemaSwaps = schemaSwaps
.stream()
.map(InternalSchemaSwap::toString)
.collect(Collectors.joining(",", "[", "]"));
throw new IllegalArgumentException("SchemaSwap annotation error " + error + ": " + umatchedSchemaSwaps);
private void validateRemainingSchemaSwaps(InternalSchemaSwaps schemaSwaps) {
String unmatchedSchemaSwaps = schemaSwaps.getUnusedSwaps()
.map(Object::toString)
.collect(Collectors.joining(","));
if (!unmatchedSchemaSwaps.isEmpty()) {
throw new IllegalArgumentException("Unmatched SchemaSwaps: " + unmatchedSchemaSwaps);
}
}

private T internalFromImpl(TypeDef definition, Set<String> visited, List<InternalSchemaSwap> schemaSwaps, String... ignore) {
private T internalFromImpl(TypeDef definition, Set<String> visited, InternalSchemaSwaps schemaSwaps, String... ignore) {
final B builder = newBuilder();
Set<String> ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore))
: Collections
Expand All @@ -215,19 +244,7 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, List<Interna
boolean preserveUnknownFields = (definition.getFullyQualifiedName() != null &&
definition.getFullyQualifiedName().equals(JSON_NODE_TYPE));

List<InternalSchemaSwap> newSchemaSwaps = definition
.getAnnotations()
.stream()
.filter(a -> a.getClassRef().getFullyQualifiedName().equals(ANNOTATION_SCHEMA_SWAP))
.map(this::extractSchemaSwap)
.collect(Collectors.toList());

schemaSwaps.addAll(newSchemaSwaps);

final Set<InternalSchemaSwap> currentSchemaSwaps = schemaSwaps
.stream()
.filter(iss -> iss.getOriginalType().getFullyQualifiedName().equals(definition.getFullyQualifiedName()))
.collect(Collectors.toSet());
definition.getAnnotations().forEach(annotation -> extractSchemaSwaps(definition.toReference(), annotation, schemaSwaps));

// index potential accessors by name for faster lookup
final Map<String, Method> accessors = indexPotentialAccessors(definition);
Expand All @@ -239,11 +256,9 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, List<Interna
continue;
}

final PropertyFacade facade = new PropertyFacade(property, accessors, currentSchemaSwaps);
ClassRef potentialSchemaSwap = schemaSwaps.lookupAndMark(definition.toReference(), name).orElse(null);
final PropertyFacade facade = new PropertyFacade(property, accessors, potentialSchemaSwap);
final Property possiblyRenamedProperty = facade.process();
final Set<InternalSchemaSwap> matchedSchemaSwaps = facade.getMatchedSchemaSwaps();
currentSchemaSwaps.removeAll(matchedSchemaSwaps);
schemaSwaps.removeAll(matchedSchemaSwaps);
name = possiblyRenamedProperty.getName();

if (facade.required) {
Expand All @@ -267,7 +282,6 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, List<Interna
addProperty(possiblyRenamedProperty, builder, possiblyUpdatedSchema);
}

validateRemainingSchemaSwaps("unmatched field", currentSchemaSwaps.stream().collect(Collectors.toList()));
return build(builder, required, preserveUnknownFields);
}

Expand Down Expand Up @@ -383,8 +397,6 @@ public String toString() {

private static class PropertyFacade {
private final List<PropertyOrAccessor> propertyOrAccessors = new ArrayList<>(4);
private final Set<InternalSchemaSwap> schemaSwaps;
private final Set<InternalSchemaSwap> matchedSchemaSwaps;
private String renamedTo;
private String description;
private boolean required;
Expand All @@ -395,10 +407,8 @@ private static class PropertyFacade {
private String descriptionContributedBy;
private TypeRef schemaFrom;

public PropertyFacade(Property property, Map<String, Method> potentialAccessors, Set<InternalSchemaSwap> schemaSwaps) {
public PropertyFacade(Property property, Map<String, Method> potentialAccessors, ClassRef schemaSwap) {
original = property;
this.schemaSwaps = schemaSwaps;
this.matchedSchemaSwaps = new HashSet<>();
final String capitalized = property.getNameCapitalized();
final String name = property.getName();
propertyOrAccessors.add(PropertyOrAccessor.fromProperty(property));
Expand All @@ -414,21 +424,12 @@ public PropertyFacade(Property property, Map<String, Method> potentialAccessors,
if (method != null) {
propertyOrAccessors.add(PropertyOrAccessor.fromMethod(method, name));
}
schemaFrom = schemaSwap;
}

public Property process() {
final String name = original.getName();

Optional<InternalSchemaSwap> currentSchemaSwap = schemaSwaps
.stream()
.filter(iss -> iss.getFieldName().equals(name))
.findFirst();

currentSchemaSwap.ifPresent(iss -> {
schemaFrom = iss.targetType;
matchedSchemaSwaps.add(iss);
});

propertyOrAccessors.forEach(p -> {
p.process();
final String contributorName = p.toString();
Expand Down Expand Up @@ -471,10 +472,6 @@ public Property process() {
return new Property(original.getAnnotations(), typeRef, finalName,
original.getComments(), original.getModifiers(), original.getAttributes());
}

public Set<InternalSchemaSwap> getMatchedSchemaSwaps() {
return this.matchedSchemaSwaps;
}
}

private boolean isPotentialAccessor(Method method) {
Expand Down Expand Up @@ -547,10 +544,10 @@ private String extractUpdatedNameFromJacksonPropertyIfPresent(Property property)
* @return the structural schema associated with the specified property
*/
public T internalFrom(String name, TypeRef typeRef) {
return internalFromImpl(name, typeRef, new HashSet<>(), new ArrayList<>());
return internalFromImpl(name, typeRef, new HashSet<>(), new InternalSchemaSwaps());
}

private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, List<InternalSchemaSwap> schemaSwaps) {
private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, InternalSchemaSwaps schemaSwaps) {
// Note that ordering of the checks here is meaningful: we need to check for complex types last
// in case some "complex" types are handled specifically
if (typeRef.getDimensions() > 0 || io.sundr.model.utils.Collections.isCollection(typeRef)) { // Handle Collections & Arrays
Expand Down Expand Up @@ -611,7 +608,7 @@ private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, Li
// Flag to detect cycles
private boolean resolving = false;

private T resolveNestedClass(String name, TypeDef def, Set<String> visited, List<InternalSchemaSwap> schemaSwaps) {
private T resolveNestedClass(String name, TypeDef def, Set<String> visited, InternalSchemaSwaps schemaSwaps) {
if (!resolving) {
visited.clear();
resolving = true;
Expand Down
@@ -0,0 +1,133 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.crd.generator;

import io.sundr.model.ClassRef;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Stream;

public class InternalSchemaSwaps {
private final Map<Key, Value> swaps = new HashMap<>();

public void registerSwap(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType) {
Value value = new Value(definitionType, originalType, fieldName, targetType);
swaps.put(new Key(originalType, fieldName), value);
}

public Stream<Value> getUnusedSwaps() {
return swaps.values().stream().filter(value -> !value.used);
}

public Optional<ClassRef> lookupAndMark(ClassRef originalType, String name) {
Value value = swaps.get(new Key(originalType, name));
if (value != null) {
value.markUsed();
return Optional.of(value.getTargetType());
} else {
return Optional.empty();
}
}

private static class Key {
private final ClassRef originalType;
private final String fieldName;

public Key(ClassRef originalType, String fieldName) {
this.originalType = originalType;
this.fieldName = fieldName;
}

public ClassRef getOriginalType() {
return originalType;
}

public String getFieldName() {
return fieldName;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Key key = (Key) o;
return Objects.equals(originalType, key.originalType) && Objects.equals(fieldName, key.fieldName);
}

@Override
public int hashCode() {
return Objects.hash(originalType, fieldName);
}

@Override
public String toString() {
return new StringJoiner(", ", Key.class.getSimpleName() + "[", "]")
.add("originalType=" + originalType)
.add("fieldName='" + fieldName + "'")
.toString();
}
}

public static class Value {
private final ClassRef originalType;
private final String fieldName;
private final ClassRef targetType;
private boolean used;
private final ClassRef definitionType;

public Value(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType) {
this.definitionType = definitionType;
this.originalType = originalType;
this.fieldName = fieldName;
this.targetType = targetType;
this.used = false;
}

private void markUsed() {
this.used = true;
}

public ClassRef getOriginalType() {
return originalType;
}

public String getFieldName() {
return fieldName;
}

public ClassRef getTargetType() {
return targetType;
}

public boolean isUsed() {
return used;
}

@Override
public String toString() {
return "@SchemaSwap(originalType=" + originalType + ", fieldName=\"" + fieldName + "\", targetType=" + targetType
+ ") on " + definitionType;
}
}
}
@@ -0,0 +1,25 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.crd.example.extraction;

import io.fabric8.crd.generator.annotation.SchemaSwap;
import io.fabric8.kubernetes.client.CustomResource;

@SchemaSwap(originalType = SchemaSwapSpec.SomeObject.class, fieldName = "shouldBeString", targetType = String.class)
@SchemaSwap(originalType = SchemaSwapSpec.AnotherObject.class, fieldName = "shouldBeInt", targetType = Integer.class)
public class MultipleSchemaSwaps extends CustomResource<SchemaSwapSpec, Void> {

}

0 comments on commit a1dd23a

Please sign in to comment.