Skip to content

Commit

Permalink
ARTEMIS-3932 Deprecate and replace addAddressSettings with json version
Browse files Browse the repository at this point in the history
As I worked through implementing a more generic JSON marshaller, I tried using reflection through BeanUtils and other ways
however the endresult was always worse as there were a few caveats that were not as easy to accomplish.

For that reason I went to a declarative appraoch where I define a meta-data object on AddressSettings and AddressSettingsInfo and
reuse the metadata in a few other places.
  • Loading branch information
clebertsuconic authored and jbertram committed Nov 27, 2023
1 parent 8e68bb1 commit 9a7a6f8
Show file tree
Hide file tree
Showing 26 changed files with 1,830 additions and 1,395 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.activemq.artemis.utils.bean;

import java.io.StringReader;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;

import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.json.JsonObject;
import org.apache.activemq.artemis.json.JsonObjectBuilder;
import org.apache.activemq.artemis.utils.JsonLoader;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Receives a metadata about a class with methods to read, write and certain gates.
* And provides a generic logic to convert to and from JSON.
* <p>
* As a historical context the first try to make a few objects more dynamic (e.g AddressSettings) was
* around BeanUtils however there was some implicit logic on when certain settins were Null or default values.
* for that reason I decided for a meta-data approach where extra semantic could be applied for each individual attributes
* rather than a generic BeanUtils parser.
*/
public class MetaBean<T> {

private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private CopyOnWriteArrayList<MetaData<T>> metaData = new CopyOnWriteArrayList<>();

/**
* Accepted types:
* String.class
* SimpleString.class
* Integer.class
* Long.class
* Double.class
* Float.class
* Enumerations
*/
public MetaBean add(Class type,
String name,
BiConsumer<T, ? extends Object> setter,
Function<T, ? extends Object> getter,
Predicate<T> gate) {
if (type != String.class && type != SimpleString.class && type != Integer.class && type != Long.class && type != Double.class && type != Float.class && type != Boolean.class && !Enum.class.isAssignableFrom(type)) {
throw new IllegalArgumentException("invalid type " + type);
}

metaData.add(new MetaData(type, name, setter, getter, gate));
return this;
}

public <Z extends Object> MetaBean add(Class<Z> type,
String name,
BiConsumer<T, Z> setter,
Function<T, Z> getter) {
return add(type, name, setter, getter, null);
}

public JsonObject toJSON(T object, boolean ignoreNullAttributes) {
JsonObjectBuilder builder = JsonLoader.createObjectBuilder();
parseToJSON(object, builder, ignoreNullAttributes);
return builder.build();
}

public void parseToJSON(T object, JsonObjectBuilder builder, boolean ignoreNullAttributes) {
logger.debug("Parsing object {}", object);
this.forEach((type, name, setter, getter, gate) -> {
logger.debug("Parsing {} {} {} {} {}", type, name, setter, getter, gate);
Object value = getter.apply(object);
if (ignoreNullAttributes && value == null) {
logger.debug("Ignoring null attribute {}", name);
return;
}
if (gate == null || gate.test(object)) {

if (logger.isTraceEnabled()) {

if (gate != null) {
logger.trace("Gate passed for {}", name);
}

if (value == null) {
logger.debug("Result for {} = IS NULL", name);
} else {
logger.debug("Result for {} = {}, type={}", name, value, value.getClass());
}
}

if (value == null) {
logger.trace("Setting {} as null", name);
builder.addNull(name);
} else if (type == String.class || type == SimpleString.class) {
logger.trace("Setting {} as String {}", name, value);
builder.add(name, String.valueOf(value));
} else if (Number.class.isAssignableFrom(type) && value instanceof Number) {
if (value instanceof Double || value instanceof Float) {
logger.trace("Setting {} as double {}", name, value);
builder.add(name, ((Number) value).doubleValue());
} else {
logger.trace("Setting {} as long {}", name, value);
builder.add(name, ((Number) value).longValue());
}
} else if (type == Boolean.class) {
builder.add(name, (Boolean) value);
} else if (Enum.class.isAssignableFrom(type)) {
// I know this is the same as the default else clause further down
// but i wanted to have a separate branch in case we have to deal with it later
builder.add(name, String.valueOf(value));
} else {
builder.add(name, String.valueOf(value));
}
} else {
logger.debug("Gate ignored on {}", name);
}
});
}

/** Generates a random Object using the setters for testing purposes. */
public void setRandom(T randomObject) {
forEach((type, name, setter, getter, gate) -> {
if (Enum.class.isAssignableFrom(type)) {
Object[] enumValues = type.getEnumConstants();
int randomInt = RandomUtil.randomInterval(0, enumValues.length - 1);
setter.accept(randomObject, enumValues[randomInt]);
} else if (type == String.class) {
setter.accept(randomObject, RandomUtil.randomString());
} else if (type == SimpleString.class) {
setter.accept(randomObject, RandomUtil.randomSimpleString());
} else if (type == Integer.class) {
setter.accept(randomObject, RandomUtil.randomPositiveInt());
} else if (type == Long.class) {
setter.accept(randomObject, RandomUtil.randomPositiveLong());
} else if (type == Double.class) {
setter.accept(randomObject, RandomUtil.randomDouble());
} else if (type == Float.class) {
setter.accept(randomObject, RandomUtil.randomFloat());
} else if (type == Boolean.class) {
setter.accept(randomObject, RandomUtil.randomBoolean());
}
});
}

public void copy(T source, T target) {
forEach((type, name, setter, getter, gate) -> {
Object sourceAttribute = getter.apply(source);
setter.accept(target, sourceAttribute);
});
}

public void forEach(MetadataListener listener) {
metaData.forEach(m -> {
try {
listener.metaItem(m.type, m.name, m.setter, m.getter, m.gate);
} catch (Throwable e) {
logger.warn("Error parsing {}", m, e);
throw new RuntimeException("Error while parsing " + m, e);
}
});
}

public void fromJSON(T resultObject, String jsonString) {

logger.debug("Parsing JSON {}", jsonString);

JsonObject json = JsonLoader.readObject(new StringReader(jsonString));

this.forEach((type, name, setter, getter, gate) -> {
if (json.containsKey(name)) {
if (json.isNull(name)) {
setter.accept(resultObject, null);
} else if (type == String.class) {
setter.accept(resultObject, json.getString(name));
} else if (type == SimpleString.class) {
setter.accept(resultObject, SimpleString.toSimpleString(json.getString(name)));
} else if (type == Integer.class) {
setter.accept(resultObject, json.getInt(name));
} else if (type == Long.class) {
setter.accept(resultObject, json.getJsonNumber(name).longValue());
} else if (type == Double.class) {
setter.accept(resultObject, json.getJsonNumber(name).doubleValue());
} else if (type == Float.class) {
setter.accept(resultObject, json.getJsonNumber(name).numberValue().floatValue());
} else if (type == Boolean.class) {
setter.accept(resultObject, json.getBoolean(name));
} else if (Enum.class.isAssignableFrom(type)) {
String value = json.getString(name);
Object enumValue = Enum.valueOf(type, value);
setter.accept(resultObject, enumValue);
}
}
});
}

static class MetaData<T> {

Class type;
String name;
BiConsumer<T, ?> setter;
Function<T, ?> getter;
Predicate<?> gate;

<Z> MetaData(Class<Z> type,
String name,
BiConsumer<T, Object> setter,
Function<T, Object> getter,
Predicate<T> gate) {
this.type = type;
this.name = name;
this.setter = setter;
this.getter = getter;
this.gate = gate;
}

@Override
public String toString() {
return "MetaData{" + "type=" + type + ", name='" + name + '\'' + '}';
}
}

public interface MetadataListener<T> {

void metaItem(Class type,
String name,
BiConsumer<T, Object> setter,
Function<T, Object> getter,
Predicate<Object> gate);
}

}

0 comments on commit 9a7a6f8

Please sign in to comment.