From: danielhanrahan Date: Wed, 3 Dec 2025 02:35:13 +0000 (+0000) Subject: Decouple policy-common from clamp X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=638c82eb05843bd145651d6cba3564c9f517755d;p=policy%2Fclamp.git Decouple policy-common from clamp - Copy used files from policy/common repo into clamp repo - Copy unit tests of those files - Update POMs to use the new internal policy-common module Issue-ID: POLICY-5494 Signed-off-by: danielhanrahan Change-Id: Ia58a1c56b72518adc09a19a81968f8091142f042 --- diff --git a/common/pom.xml b/common/pom.xml index 5fa183b01..86f698c6f 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -40,11 +40,6 @@ policy-models ${project.version} - - org.onap.policy.common - message-bus - ${policy.common.version} - jakarta.ws.rs jakarta.ws.rs-api diff --git a/models/pom.xml b/models/pom.xml index 0524f959f..54d33169b 100644 --- a/models/pom.xml +++ b/models/pom.xml @@ -39,22 +39,6 @@ policy-models ${project.version} - - org.onap.policy.common - common-parameters - ${policy.common.version} - - - org.onap.policy.common - utils - ${policy.common.version} - - - org.onap.policy.common - utils-test - ${policy.common.version} - test - com.google.code.gson gson diff --git a/participant/participant-intermediary/pom.xml b/participant/participant-intermediary/pom.xml index 5b6102fe8..48e8d3573 100644 --- a/participant/participant-intermediary/pom.xml +++ b/participant/participant-intermediary/pom.xml @@ -35,26 +35,6 @@ - - org.onap.policy.common - common-parameters - ${policy.common.version} - - - org.onap.policy.common - message-bus - ${policy.common.version} - - - org.onap.policy.common - policy-endpoints - ${policy.common.version} - - - org.onap.policy.common - utils - ${policy.common.version} - io.micrometer micrometer-core diff --git a/policy-common/pom.xml b/policy-common/pom.xml new file mode 100644 index 000000000..ebfd23512 --- /dev/null +++ b/policy-common/pom.xml @@ -0,0 +1,167 @@ + + + + + 4.0.0 + + + org.onap.policy.clamp + policy-clamp + 9.0.0-SNAPSHOT + + + policy-common + ${project.artifactId} + + + + com.google.code.gson + gson + + + commons-io + commons-io + + + commons-net + commons-net + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-context + + + io.opentelemetry.instrumentation + opentelemetry-kafka-clients-2.6 + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + jakarta.validation + jakarta.validation-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.apache.kafka + kafka-clients + provided + + + org.apache.commons + commons-collections4 + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-jexl3 + + + org.hibernate.orm + hibernate-core + + + org.projectlombok + lombok + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.springframework + spring-core + + + org.springframework + spring-web + + + org.yaml + snakeyaml + + + com.google.re2j + re2j + provided + + + com.openpojo + openpojo + compile + + + org.hamcrest + hamcrest + compile + + + org.junit.jupiter + junit-jupiter-api + compile + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + org.springframework + spring-test + test + + + diff --git a/policy-common/src/main/java/org/onap/policy/common/capabilities/Lockable.java b/policy-common/src/main/java/org/onap/policy/common/capabilities/Lockable.java new file mode 100644 index 000000000..cc9da0a90 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/capabilities/Lockable.java @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * policy-core + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.capabilities; + +/** + * Interface for classes that supports lock/unlock operations. + */ +public interface Lockable { + + /** + * Locks this entity. + * + * @return true is the lock operation was successful, false otherwise + */ + public boolean lock(); + + /** + * Unlocks this entity. + * + * @return true is the unlock operation was successful, false otherwise + */ + public boolean unlock(); + + /** + * Checks if this entity is locked. + * + * @return true if the entity is in a locked state, false otherwise + */ + public boolean isLocked(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/capabilities/Startable.java b/policy-common/src/main/java/org/onap/policy/common/capabilities/Startable.java new file mode 100644 index 000000000..0ce4da543 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/capabilities/Startable.java @@ -0,0 +1,62 @@ +/* + * ============LICENSE_START======================================================= + * policy-core + * ================================================================================ + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.capabilities; + +/** + * Interface for classes that supports start-like operations. + */ +public interface Startable { + + /** + * Start operation. This operation starts the entity. + * + * @return boolean. true if the start operation was successful, otherwise false. + * @throws IllegalStateException if the element is in a state that conflicts with the start + * operation. + */ + boolean start(); + + /** + * Stop operation. The entity can be restarted again by invoking the start operation. + * + * @return boolean. true if the stop operation was successful, otherwise false. + * @throws IllegalStateException if the element is in a state that conflicts with the stop + * operation. + */ + boolean stop(); + + /** + * shutdown operation. The terminate operation yields the entity unusuable. It cannot be + * (re)started. + * + * @throws IllegalStateException if the element is in a state that conflicts with the stop + * operation. + */ + void shutdown(); + + /** + * Checks if the entity is alive. + * + * @return boolean. true if alive, otherwise false + */ + boolean isAlive(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/JsonListener.java b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/JsonListener.java new file mode 100644 index 000000000..90c8338fb --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/JsonListener.java @@ -0,0 +1,72 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.endpoints.listeners; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Listens for messages received on a topic, in JSON format, decodes them into a + * {@link StandardCoderObject}, and then offers the objects to the subclass. + */ +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class JsonListener implements TopicListener { + private static final Logger logger = LoggerFactory.getLogger(JsonListener.class); + + /** + * Used to decode the event. + */ + private static final Coder coder = new StandardCoder(); + + @Override + public void onTopicEvent(CommInfrastructure infra, String topic, String event) { + // decode from JSON into a standard object + StandardCoderObject sco; + try { + sco = coder.decode(event, StandardCoderObject.class); + + } catch (CoderException e) { + logger.warn("unable to decode: {}", event, e); + return; + } + + onTopicEvent(infra, topic, sco); + } + + /** + * Indicates that a standard object was received. + * + * @param infra infrastructure with which the message was received + * @param topic topic on which the message was received + * @param sco the standard object that was received + */ + public abstract void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java new file mode 100644 index 000000000..6639b4162 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.endpoints.listeners; + +import java.util.concurrent.ConcurrentHashMap; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dispatches standard objects to listeners, based on the message type extracted from the + * message. Only one listener may be registered for a given type. + */ +public class MessageTypeDispatcher extends JsonListener { + private static final Logger logger = LoggerFactory.getLogger(MessageTypeDispatcher.class); + + /** + * Name of the message field, which may be hierarchical. + */ + private final Object[] messageFieldNames; + + /** + * Name of the message field, joined with "." - for logging. + */ + private final String fullMessageFieldName; + + /** + * Maps a message type to its listener. + */ + private final ConcurrentHashMap> type2listener = new ConcurrentHashMap<>(); + + /** + * Constructs the object. + * + * @param messageFieldNames name of the message field, which may be hierarchical + */ + public MessageTypeDispatcher(String... messageFieldNames) { + this.messageFieldNames = messageFieldNames; + this.fullMessageFieldName = String.join(".", messageFieldNames); + } + + /** + * Registers a listener for a certain type of message. + * + * @param type type of message of interest to the listener + * @param listener listener to register + */ + public void register(String type, ScoListener listener) { + type2listener.put(type, listener); + } + + /** + * Unregisters the listener associated with the specified message type. + * + * @param type type of message whose listener is to be unregistered + */ + public void unregister(String type) { + type2listener.remove(type); + } + + @Override + public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco) { + // extract the message type + final var type = sco.getString(messageFieldNames); + if (type == null) { + logger.warn("unable to extract {}: {}", fullMessageFieldName, sco); + return; + } + + // dispatch the message + ScoListener listener = type2listener.get(type); + if (listener == null) { + logger.info("discarding event of type {}", type); + return; + } + + listener.onTopicEvent(infra, topic, sco); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/ScoListener.java b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/ScoListener.java new file mode 100644 index 000000000..bdb30eb15 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/ScoListener.java @@ -0,0 +1,87 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.endpoints.listeners; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Listens for receipt of a {@link StandardCoderObject}, translating it into an object of + * the appropriate type, and then passing it to the subclass. + * + * @param type of message/POJO this handles + */ +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class ScoListener { + + private static final Logger logger = LoggerFactory.getLogger(ScoListener.class); + + /** + * Used to translate the standard object to an object of type "T". + */ + private static final Coder coder = new StandardCoder(); + + /** + * Class of message this handles. + */ + private final Class clazz; + + /** + * Receives an event, translates it into the desired type of object, and passes it to + * the subclass. + * + * @param infra infrastructure with which the message was received + * @param topic topic on which the message was received + * @param sco event that was received + */ + public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco) { + // translate the event to the desired object type + final T msg; + try { + msg = coder.fromStandard(sco, clazz); + + } catch (CoderException e) { + logger.warn("unable to decode {}: {}", clazz.getName(), sco, e); + return; + } + + onTopicEvent(infra, topic, sco, msg); + } + + /** + * Indicates that a message was received. + * + * @param infra infrastructure with which the message was received + * @param topic topic on which the message was received + * @param sco event that was received + * @param message message that was received + */ + public abstract void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco, T message); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/DoubleConverter.java b/policy-common/src/main/java/org/onap/policy/common/gson/DoubleConverter.java new file mode 100644 index 000000000..4d10bd133 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/DoubleConverter.java @@ -0,0 +1,116 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * Converter for Double values. By default, GSON treats all Objects that are numbers, as + * Double. This converts Doubles to Integer or Long, if possible. It converts stand-alone + * Doubles, as well as those found within Arrays and Maps. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DoubleConverter { + + /** + * Performs in-place conversion of all values in a list. + * + * @param list the list whose values are to be converted + */ + public static void convertFromDouble(List list) { + if (list == null) { + return; + } + + List original = new ArrayList<>(list); + + list.clear(); + original.forEach(item -> list.add(convertFromDouble(item))); + } + + /** + * Performs in-place conversion of all values in a map. + * + * @param map the map whose values are to be converted + */ + public static void convertFromDouble(Map map) { + if (map == null) { + return; + } + + Set> set = map.entrySet(); + + for (Entry entry : set) { + entry.setValue(convertFromDouble(entry.getValue())); + } + } + + /** + * Converts a value. If the value is a List, then it recursively converts the + * entries of the List. Likewise with a map, however, the map is converted in place. + * + * @param value value to be converted + * @return the converted value + */ + @SuppressWarnings({"unchecked"}) + public static Object convertFromDouble(Object value) { + if (value == null) { + return value; + } + + if (value instanceof List) { + convertFromDouble((List) value); + return value; + } + + if (value instanceof Map) { + convertFromDouble((Map) value); + return value; + } + + if (!(value instanceof Double)) { + return value; + } + + Double num = (Double) value; + var longval = num.longValue(); + + if (Double.compare(num.doubleValue(), longval) != 0) { + // it isn't integral - return unchanged value + return value; + } + + // it's integral - determine if it's an integer or a long + var intval = (int) longval; + + if (intval == longval) { + return intval; + } + + return longval; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java b/policy-common/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java new file mode 100644 index 000000000..a693b7f40 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java @@ -0,0 +1,161 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provider that serializes and de-serializes JSON via gson. + */ +@Provider +@Consumes(MediaType.WILDCARD) +@Produces(MediaType.WILDCARD) +public class GsonMessageBodyHandler implements MessageBodyReader, MessageBodyWriter { + + public static final Logger logger = LoggerFactory.getLogger(GsonMessageBodyHandler.class); + + /** + * Object to be used to serialize and de-serialize. + */ + @Getter(AccessLevel.PROTECTED) + private final Gson gson; + + /** + * Constructs the object, using a Gson object that translates Doubles inside of Maps + * into Integer/Long, where possible. + */ + public GsonMessageBodyHandler() { + this(configBuilder(new GsonBuilder()).create()); + } + + /** + * Constructs the object. + * + * @param gson the Gson object to be used to serialize and de-serialize + */ + public GsonMessageBodyHandler(Gson gson) { + this.gson = gson; + + logger.info("Using GSON for REST calls"); + } + + /** + * Configures a builder with the adapters normally used by this handler (e.g., mapper + * that converts Double to Integer). + * + * @param builder builder to be configured + * @return the configured builder + */ + public static GsonBuilder configBuilder(GsonBuilder builder) { + return builder.disableHtmlEscaping().registerTypeAdapterFactory(new MapDoubleAdapterFactory()) + .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter()) + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter()) + .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()) + .registerTypeAdapter(OffsetTime.class, new OffsetTimeTypeAdapter()) + .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter()) + .registerTypeAdapter(ZoneOffset.class, new ZoneOffsetTypeAdapter()); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return canHandle(mediaType); + } + + @Override + public long getSize(Object object, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(Object object, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException { + + try (var writer = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) { + Type jsonType = (type.equals(genericType) ? type : genericType); + gson.toJson(object, jsonType, writer); + } + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return canHandle(mediaType); + } + + /** + * Determines if this provider can handle the given media type. + * + * @param mediaType the media type of interest + * @return {@code true} if this provider handles the given media type, {@code false} + * otherwise + */ + private boolean canHandle(MediaType mediaType) { + if (mediaType == null) { + return true; + } + + String subtype = mediaType.getSubtype(); + + if ("json".equalsIgnoreCase(subtype) || "javascript".equals(subtype)) { + return true; + } + + return subtype.endsWith("+json") || "x-json".equals(subtype) || "x-javascript".equals(subtype); + } + + @Override + public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { + + try (var streamReader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) { + Type jsonType = (type.equals(genericType) ? type : genericType); + return gson.fromJson(streamReader, jsonType); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java new file mode 100644 index 000000000..c38a3e9b5 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.time.Instant; + +/** + * GSON Type Adapter for "Instant" fields, that encodes them as milliseconds. + */ +public class InstantAsMillisTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Instant value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.toEpochMilli()); + } + } + + @Override + public Instant read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + var millis = in.nextLong(); + return Instant.ofEpochMilli(millis); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java new file mode 100644 index 000000000..bad66af36 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.Instant; + +/** + * GSON Type Adapter for "Instant" fields, that uses the standard ISO_INSTANT formatter. + */ +public class InstantTypeAdapter extends StringTypeAdapter { + + /** + * Constructs an adapter. + */ + public InstantTypeAdapter() { + super("date", Instant::parse, Instant::toString); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java new file mode 100644 index 000000000..cb959c43f --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java @@ -0,0 +1,104 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.JsonElement; +import java.lang.reflect.GenericArrayType; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Excludes all fields from serialization/deserialization, if the class is managed. + */ +public class JacksonExclusionStrategy implements ExclusionStrategy { + + /** + * Classes that are explicitly not managed by the GSON jackson adapters. + */ + // @formatter:off + private static final Set> unmanaged = new HashSet<>(Arrays.asList( + boolean.class, + byte.class, + short.class, + int.class, + long.class, + float.class, + double.class, + char.class, + Boolean.class, + Byte.class, + Short.class, + Integer.class, + Long.class, + Float.class, + Double.class, + Character.class, + String.class)); + // @formatter:on + + /** + * Classes whose subclasses are explicitly not managed by the GSON jackson adapters. + */ + // @formatter:off + private static final Set> unmanagedSuper = new HashSet<>(Arrays.asList( + GenericArrayType.class, + Map.class, + Collection.class, + JsonElement.class)); + // @formatter:on + + @Override + public boolean shouldSkipField(FieldAttributes attrs) { + return isManaged(attrs.getDeclaringClass()); + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } + + /** + * Determines if a class is managed by this adapter, which typically means that it is + * not a generic class such as {@link JsonElement} or some type of collection. + * + * @param clazz the class to be examined + * @return {@code true} if the class is managed by this adapter, {@code false} + * otherwise + */ + public static boolean isManaged(Class clazz) { + if (clazz.isArray() || clazz.isEnum() || clazz.isPrimitive() || unmanaged.contains(clazz)) { + return false; + } + + for (Class sup : unmanagedSuper) { + if (sup.isAssignableFrom(clazz)) { + return false; + } + } + + return true; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonFieldAdapterFactory.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonFieldAdapterFactory.java new file mode 100644 index 000000000..18157b03c --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonFieldAdapterFactory.java @@ -0,0 +1,101 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import org.onap.policy.common.gson.internal.ClassWalker; +import org.onap.policy.common.gson.internal.Deserializer; +import org.onap.policy.common.gson.internal.FieldDeserializer; +import org.onap.policy.common.gson.internal.FieldSerializer; +import org.onap.policy.common.gson.internal.JacksonTypeAdapter; +import org.onap.policy.common.gson.internal.Serializer; + +/** + * Factory that serializes/deserializes class fields following the normal behavior of + * jackson. Supports the following annotations: + *
    + *
  • GsonJsonIgnore
  • + *
  • GsonJsonProperty
  • + *
+ * + *

Note: {@link JacksonExclusionStrategy} must also be registered with the gson object. + */ +public class JacksonFieldAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class clazz = type.getRawType(); + + if (!JacksonExclusionStrategy.isManaged(clazz)) { + return null; + } + + var data = new ClassWalker(); + data.walkClassHierarchy(clazz); + + if (data.getInProps(Field.class).isEmpty() && data.getOutProps(Field.class).isEmpty()) { + // no fields to serialize + return null; + } + + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + List sers = makeSerializers(gson, data); + List desers = makeDeserializers(gson, data); + + return new JacksonTypeAdapter<>(gson, delegate, sers, desers); + } + + /** + * Creates a complete list of serializers. + * + * @param gson the associated gson object + * @param data data used to configure the serializers + * @return a list of all serializers + */ + private List makeSerializers(Gson gson, ClassWalker data) { + List ser = new ArrayList<>(); + + data.getOutProps(Field.class).forEach(field -> ser.add(new FieldSerializer(gson, field))); + + return ser; + } + + /** + * Creates a complete list of deserializers. + * + * @param gson the associated gson object + * @param data data used to configure the deserializers + * @return a list of all deserializers + */ + private List makeDeserializers(Gson gson, ClassWalker data) { + List deser = new ArrayList<>(); + + data.getInProps(Field.class).forEach(field -> deser.add(new FieldDeserializer(gson, field))); + + return deser; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonHandler.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonHandler.java new file mode 100644 index 000000000..420fbdb29 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonHandler.java @@ -0,0 +1,67 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provider used to serialize and deserialize policy objects via gson using jackson + * default behaviors and annotations. + */ +public class JacksonHandler extends GsonMessageBodyHandler { + + public static final Logger logger = LoggerFactory.getLogger(JacksonHandler.class); + + /** + * Constructs the object. + */ + public JacksonHandler() { + this(configBuilder(new GsonBuilder()).create()); + } + + /** + * Constructs the object. + * + * @param gson the Gson object to be used to serialize and de-serialize + */ + public JacksonHandler(Gson gson) { + super(gson); + logger.info("Using GSON with Jackson behaviors for REST calls"); + } + + /** + * Configures a builder with the adapters normally used by this handler (e.g., + * adapters for GsonJsonXxx annotations). + * + * @param builder builder to be configured + * @return the configured builder + */ + public static GsonBuilder configBuilder(GsonBuilder builder) { + return builder.registerTypeAdapterFactory(new JacksonFieldAdapterFactory()) + .registerTypeAdapterFactory(new JacksonMethodAdapterFactory()) + .registerTypeAdapterFactory(new MapDoubleAdapterFactory()) + .setExclusionStrategies(new JacksonExclusionStrategy()) + .disableHtmlEscaping(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonMethodAdapterFactory.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonMethodAdapterFactory.java new file mode 100644 index 000000000..b74140042 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonMethodAdapterFactory.java @@ -0,0 +1,120 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.onap.policy.common.gson.internal.AnyGetterSerializer; +import org.onap.policy.common.gson.internal.AnySetterDeserializer; +import org.onap.policy.common.gson.internal.ClassWalker; +import org.onap.policy.common.gson.internal.Deserializer; +import org.onap.policy.common.gson.internal.JacksonTypeAdapter; +import org.onap.policy.common.gson.internal.MethodDeserializer; +import org.onap.policy.common.gson.internal.MethodSerializer; +import org.onap.policy.common.gson.internal.Serializer; + +/** + * Factory that serializes/deserializes class methods following the normal behavior of + * jackson. Supports the following annotations: + *

    + *
  • GsonJsonIgnore
  • + *
  • GsonJsonProperty
  • + *
  • GsonJsonAnyGetter
  • + *
  • GsonJsonAnySetter
  • + *
+ */ +public class JacksonMethodAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class clazz = type.getRawType(); + + if (!JacksonExclusionStrategy.isManaged(clazz)) { + return null; + } + + var data = new ClassWalker(); + data.walkClassHierarchy(clazz); + + if (data.getInProps(Method.class).isEmpty() && data.getOutProps(Method.class).isEmpty() + && data.getAnyGetter() == null && data.getAnySetter() == null) { + // no methods to serialize + return null; + } + + Set unliftedProps = new HashSet<>(); + unliftedProps.addAll(data.getInNotIgnored()); + unliftedProps.addAll(data.getOutNotIgnored()); + + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + List sers = makeSerializers(gson, data, unliftedProps); + List desers = makeDeserializers(gson, data, unliftedProps); + + return new JacksonTypeAdapter<>(gson, delegate, sers, desers); + } + + /** + * Creates a complete list of serializers. + * + * @param gson the associated gson object + * @param data data used to configure the serializers + * @param unliftedProps properties that should not be lowered by "any-getters" + * @return a list of all serializers + */ + private List makeSerializers(Gson gson, ClassWalker data, Set unliftedProps) { + List ser = new ArrayList<>(); + + if (data.getAnyGetter() != null) { + ser.add(new AnyGetterSerializer(gson, unliftedProps, data.getAnyGetter())); + } + + data.getOutProps(Method.class).forEach(method -> ser.add(new MethodSerializer(gson, method))); + + return ser; + } + + /** + * Creates a complete list of deserializers. + * + * @param gson the associated gson object + * @param data data used to configure the deserializers + * @param unliftedProps properties that should not be lifted by "any-setters" + * @return a list of all deserializers + */ + private List makeDeserializers(Gson gson, ClassWalker data, Set unliftedProps) { + List deser = new ArrayList<>(); + + if (data.getAnySetter() != null) { + deser.add(new AnySetterDeserializer(gson, unliftedProps, data.getAnySetter())); + } + + data.getInProps(Method.class).forEach(method -> deser.add(new MethodDeserializer(gson, method))); + + return deser; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java new file mode 100644 index 000000000..5dc597e26 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java @@ -0,0 +1,39 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * GSON Type Adapter for "LocalDateTime" fields, that uses the standard + * ISO_LOCAL_DATE_TIME formatter, by default. + */ +public class LocalDateTimeTypeAdapter extends StringTypeAdapter { + + public LocalDateTimeTypeAdapter() { + this(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + + public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) { + super("date", string -> LocalDateTime.parse(string, formatter), value -> value.format(formatter)); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTypeAdapter.java new file mode 100644 index 000000000..0f666e5ec --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTypeAdapter.java @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class LocalDateTypeAdapter extends StringTypeAdapter { + + public LocalDateTypeAdapter() { + this(DateTimeFormatter.ISO_LOCAL_DATE); + } + + public LocalDateTypeAdapter(DateTimeFormatter formatter) { + super("date", string -> LocalDate.parse(string, formatter), value -> value.format(formatter)); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java b/policy-common/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java new file mode 100644 index 000000000..057e97f82 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java @@ -0,0 +1,122 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +/** + * Adapter factory for Map<String,Object> and List<String>. By default, GSON treats all Objects, that + * are numbers, as Double. This recursively walks a map/list and converts Doubles to Integer or Long, if + * possible. + */ +public class MapDoubleAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (!isMapType(type) && !isListType(type)) { + return null; + } + + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + + return new MapAdapter<>(delegate); + } + + private boolean isMapType(TypeToken type) { + if (!Map.class.isAssignableFrom(type.getRawType())) { + return false; + } + + // only supports Map + + if (!(type.getType() instanceof ParameterizedType)) { + // untyped - assume the parameters are the correct type + return true; + } + + Type[] actualParams = ((ParameterizedType) type.getType()).getActualTypeArguments(); + + return (actualParams[0] == String.class && actualParams[1] == Object.class); + } + + private boolean isListType(TypeToken type) { + if (!List.class.isAssignableFrom(type.getRawType())) { + return false; + } + + // only supports List + + if (!(type.getType() instanceof ParameterizedType)) { + // untyped - assume the parameters are the correct type + return true; + } + + Type[] actualParams = ((ParameterizedType) type.getType()).getActualTypeArguments(); + + return (actualParams[0] == Object.class); + } + + /** + * Type adapter that performs conversion from Double to Integer/Long. + * + * @param type of object on which this works (always Map.class) + */ + private static class MapAdapter extends TypeAdapter { + + /** + * Used to perform conversion between JSON and Map<String,Object>. + */ + private final TypeAdapter delegate; + + /** + * Constructs the object. + * + * @param delegate JSON/Map converter + */ + public MapAdapter(TypeAdapter delegate) { + this.delegate = delegate; + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + delegate.write(out, value); + } + + @Override + public T read(JsonReader in) throws IOException { + var value = delegate.read(in); + + DoubleConverter.convertFromDouble(value); + + return value; + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/OffsetDateTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/OffsetDateTimeTypeAdapter.java new file mode 100644 index 000000000..3f046b010 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/OffsetDateTimeTypeAdapter.java @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +public class OffsetDateTimeTypeAdapter extends StringTypeAdapter { + + public OffsetDateTimeTypeAdapter() { + this(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + public OffsetDateTimeTypeAdapter(DateTimeFormatter formatter) { + super("date", string -> OffsetDateTime.parse(string, formatter), value -> value.format(formatter)); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/OffsetTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/OffsetTimeTypeAdapter.java new file mode 100644 index 000000000..895b9de69 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/OffsetTimeTypeAdapter.java @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.OffsetTime; +import java.time.format.DateTimeFormatter; + +public class OffsetTimeTypeAdapter extends StringTypeAdapter { + + public OffsetTimeTypeAdapter() { + this(DateTimeFormatter.ISO_OFFSET_TIME); + } + + public OffsetTimeTypeAdapter(DateTimeFormatter formatter) { + super("time", string -> OffsetTime.parse(string, formatter), value -> value.format(formatter)); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/StringTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/StringTypeAdapter.java new file mode 100644 index 000000000..224816970 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/StringTypeAdapter.java @@ -0,0 +1,76 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.function.Function; + +/** + * GSON Type Adapter for fields that are encoded as Strings. + */ +public class StringTypeAdapter extends TypeAdapter { + private final String exMessage; + private final Function deserializer; + private final Function serializer; + + /** + * Constructs an adapter. + * + * @param type type of value, used in exception messages + * @param deserializer function used to deserialize a String into a value + * @param serializer function used to serialize a value into a String + */ + public StringTypeAdapter(String type, Function deserializer, Function serializer) { + this.exMessage = "invalid " + type; + this.deserializer = deserializer; + this.serializer = serializer; + } + + @Override + public T read(JsonReader in) throws IOException { + try { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + return deserializer.apply(in.nextString()); + } + + } catch (RuntimeException e) { + throw new JsonParseException(exMessage, e); + } + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + String text = serializer.apply(value); + out.value(text); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapter.java new file mode 100644 index 000000000..60758ff36 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapter.java @@ -0,0 +1,30 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.ZoneOffset; + +public class ZoneOffsetTypeAdapter extends StringTypeAdapter { + + public ZoneOffsetTypeAdapter() { + super("zone", ZoneOffset::of, ZoneOffset::toString); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java new file mode 100644 index 000000000..928fae957 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java @@ -0,0 +1,47 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * GSON Type Adapter for "ZonedDateTime" fields, that uses the standard + * ISO_ZONED_DATE_TIME formatter. + */ +public class ZonedDateTimeTypeAdapter extends StringTypeAdapter { + + /** + * Constructs an adapter that uses the ISO_ZONED_DATE_TIME formatter. + */ + public ZonedDateTimeTypeAdapter() { + this(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + + /** + * Constructs an adapter that uses the specified formatter for reading and writing. + * + * @param formatter date-time formatter + */ + public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) { + super("date", string -> ZonedDateTime.parse(string, formatter), value -> value.format(formatter)); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java new file mode 100644 index 000000000..859f53860 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonAnyGetter annotation, but used by gson. This requires the gson + * object to be configured with the jackson default behaviors (i.e., the associated + * JacksonXxx strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface GsonJsonAnyGetter { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java new file mode 100644 index 000000000..87e0f3309 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonAnySetter annotation, but used by gson. This requires the gson + * object to be configured with the jackson default behaviors (i.e., the associated + * JacksonXxx strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface GsonJsonAnySetter { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java new file mode 100644 index 000000000..cf2d43944 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonIgnore annotation, but used by gson. This requires the gson object + * to be configured with the jackson default behaviors (i.e., the associated JacksonXxx + * strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target({FIELD, METHOD}) +public @interface GsonJsonIgnore { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java new file mode 100644 index 000000000..c31c19bb5 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Mimics Jackson JsonProperty annotation, but used by gson. This requires the gson object + * to be configured with the jackson default behaviors (i.e., the associated JacksonXxx + * strategy and adapters must be registered with the gson object). + */ +@Retention(RUNTIME) +@Target({FIELD, METHOD}) +public @interface GsonJsonProperty { + + /** + * Property name of this item when placed into a JsonObject. + * @return the item's serialized name + */ + String value() default ""; +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Adapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Adapter.java new file mode 100644 index 000000000..af4a746c9 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Adapter.java @@ -0,0 +1,360 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.re2j.Pattern; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.function.Supplier; +import lombok.Getter; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +/** + * Super class of adapters used to serialize and de-serialize an item. + */ +public class Adapter { + + /** + * Pattern to match valid identifiers. + */ + private static final Pattern VALID_NAME_PAT = Pattern.compile("[a-zA-Z_]\\w*"); + + /** + * Factory to access objects. Overridden by junit tests. + */ + private static Factory factory = new Factory(); + + /** + * Name of the property within the json structure containing the item. + */ + @Getter + private final String propName; + + /** + * Gson object that will provide the type converter. + */ + private final Gson gson; + + /** + * Converter used when reading. + */ + private final ConvInfo reader; + + /** + * Converter used when writing, allocated lazily, once an actual type is determined. + */ + private ConvInfo writer = null; + + /** + * Name of the item being lifted - used when throwing exceptions. + */ + @Getter + private final String fullName; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param field field used to access the item from within an object + */ + public Adapter(Gson gson, Field field) { + this.propName = detmPropName(field); + this.reader = new ConvInfo(TypeToken.get(field.getGenericType())); + this.gson = gson; + this.fullName = getQualifiedName(field); + + /* + * Turning off sonar, as this is required for emulation of "jackson". + */ + field.setAccessible(true); // NOSONAR + } + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param accessor method used to access the item from within an object + * @param valueType the class of value on which this operates + */ + public Adapter(Gson gson, Method accessor, Type valueType) { + boolean forSetter = (accessor.getReturnType() == void.class); + this.propName = (forSetter ? detmSetterPropName(accessor) : detmGetterPropName(accessor)); + this.reader = new ConvInfo(TypeToken.get(valueType)); + this.gson = gson; + this.fullName = getQualifiedName(accessor); + + /* + * Turning off sonar, as this is required for emulation of "jackson". + */ + accessor.setAccessible(true); // NOSONAR + } + + /** + * Converts an object to a json tree. + * + * @param object the object to be converted + * @return a json tree representing the object + */ + @SuppressWarnings("unchecked") + public JsonElement toJsonTree(Object object) { + // always use a converter for the specific subclass + Class clazz = object.getClass(); + + if (writer == null) { + // race condition here, but it's ok to overwrite a previous value + writer = new ConvInfo(TypeToken.get(clazz)); + } + + ConvInfo wtr = writer; + + TypeAdapter conv = + (wtr.clazz == clazz ? wtr.getConverter() : (TypeAdapter) gson.getAdapter(clazz)); + + return conv.toJsonTree(object); + } + + /** + * Converts a json tree to an object. + * + * @param tree the tree to be converted + * @return the object represented by the tree + */ + public Object fromJsonTree(JsonElement tree) { + return reader.getConverter().fromJsonTree(tree); + } + + /** + * Makes an error message, appending the item's full name to the message prefix. + * + * @param prefix the message prefix + * @return the error message + */ + public String makeError(String prefix) { + return (prefix + fullName); + } + + /** + * Determines if the field is managed by the walker. + * + * @param field the field to examine + * @return {@code true} if the field is managed by the walker, {@code false} otherwise + */ + public static boolean isManaged(Field field) { + return VALID_NAME_PAT.matcher(factory.getName(field)).matches(); + } + + /** + * Determines if the method is managed by the walker. + * + * @param method the method to examine + * @return {@code true} if the method is managed by the walker, {@code false} + * otherwise + */ + public static boolean isManaged(Method method) { + return VALID_NAME_PAT.matcher(factory.getName(method)).matches(); + } + + /** + * Determines the property name of an item within the json structure. + * + * @param field the item within the object + * @return the json property name for the item or {@code null} if the name is invalid + */ + public static String detmPropName(Field field) { + // use the serialized name, if specified + GsonJsonProperty prop = field.getAnnotation(GsonJsonProperty.class); + if (prop != null && !prop.value().isEmpty()) { + return prop.value(); + } + + // no name provided - use it as is + return (isManaged(field) ? factory.getName(field) : null); + } + + /** + * Determines the property name of an item, within the json structure, associated with + * a "get" method. + * + * @param method method to be invoked to get the item within the object + * @return the json property name for the item, or {@code null} if the method name is + * not valid + */ + public static String detmGetterPropName(Method method) { + + return detmPropNameCommon(method, () -> { + + if (!isManaged(method)) { + return null; + } + + String name = factory.getName(method); + + if (name.startsWith("get")) { + return name.substring(3); + + } else if (name.startsWith("is")) { + Class treturn = method.getReturnType(); + + if (treturn == boolean.class || treturn == Boolean.class) { + return name.substring(2); + } + } + + // not a valid name for a "getter" method + return null; + }); + } + + /** + * Determines the property name of an item, within the json structure, associated with + * a "set" method. + * + * @param method method to be invoked to set the item within the object + * @return the json property name for the item, or {@code null} if the method name is + * not valid + */ + public static String detmSetterPropName(Method method) { + + return detmPropNameCommon(method, () -> { + + if (!isManaged(method)) { + return null; + } + + String name = factory.getName(method); + + if (name.startsWith("set")) { + return name.substring(3); + } + + // not a valid name for a "setter" method + return null; + }); + } + + /** + * Determines the property name of an item within the json structure. + * + * @param method method to be invoked to get/set the item within the object + * @param extractor function to extract the name directly from the method name + * @return the json property name for the item, or {@code null} if the method name is + * not valid + */ + private static String detmPropNameCommon(Method method, Supplier extractor) { + + // use the property name, if specified + GsonJsonProperty propName = method.getAnnotation(GsonJsonProperty.class); + if (propName != null && !propName.value().isEmpty()) { + return propName.value(); + } + + // no name provided - must compute it from the method name + String name = extractor.get(); + + if (name == null || name.isEmpty()) { + // nothing left after stripping the prefix - invalid name + return null; + } + + // translate the first letter to lower-case + return name.substring(0, 1).toLowerCase() + name.substring(1); + } + + /** + * Gets the fully qualified name of a field. + * + * @param field field whose name is desired + * @return the field fully qualified name + */ + public static String getQualifiedName(Field field) { + return (field.getDeclaringClass().getName() + "." + factory.getName(field)); + } + + /** + * Gets the fully qualified name of a method. + * + * @param method method whose name is desired + * @return the method's fully qualified name + */ + public static String getQualifiedName(Method method) { + return (method.getDeclaringClass().getName() + "." + factory.getName(method)); + } + + /** + * Converter info. + */ + private class ConvInfo { + + /** + * Type on which the converter works. + */ + private TypeToken type; + + /** + * Class of object on which the converter works. + */ + private Class clazz; + + /** + * Converter to use, initialized lazily. + */ + private TypeAdapter conv = null; + + /** + * Constructs the object. + * + * @param type type of object to be converted + */ + public ConvInfo(TypeToken type) { + this.type = type; + this.clazz = type.getRawType(); + } + + @SuppressWarnings("unchecked") + public final TypeAdapter getConverter() { + if (conv == null) { + // race condition here, but it's ok to overwrite a previous value + this.conv = (TypeAdapter) gson.getAdapter(type); + } + + return conv; + } + } + + /** + * Factory used to access various objects. + */ + public static class Factory { + + public String getName(Field field) { + return field.getName(); + } + + public String getName(Method method) { + return method.getName(); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnyGetterSerializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnyGetterSerializer.java new file mode 100644 index 000000000..4ad924aff --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnyGetterSerializer.java @@ -0,0 +1,82 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.lang.reflect.Method; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Serializer for methods having a JsonAnyGetter annotation. + */ +public class AnyGetterSerializer extends Lifter implements Serializer { + + public static final String NOT_AN_OBJECT_ERR = "expecting a JsonObject for "; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param unliftedProps property names that should not be lifted + * @param getter method used to get the item from within an object + */ + public AnyGetterSerializer(Gson gson, Set unliftedProps, Method getter) { + super(gson, unliftedProps, getter, getter.getGenericReturnType()); + } + + @Override + public void addToTree(Object source, JsonObject target) { + // get the value from the object + Object value = invoke(source); + if (value == null) { + // nothing to lift + return; + } + + JsonElement inner = toJsonTree(value); + if (!inner.isJsonObject()) { + throw new JsonParseException(makeError(NOT_AN_OBJECT_ERR)); + } + + // lift items from inner into the target + copyLiftedItems(inner.getAsJsonObject(), target); + } + + /** + * Copies lifted items from one tree into another, without removing them from the + * source tree. + * + * @param source tree from which items are to be copied + * @param target tree into which items are to be copied + */ + private void copyLiftedItems(JsonObject source, JsonObject target) { + for (Entry ent : source.entrySet()) { + String name = ent.getKey(); + if (shouldLift(name)) { + target.add(name, ent.getValue()); + } + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnySetterDeserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnySetterDeserializer.java new file mode 100644 index 000000000..411d30cdc --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnySetterDeserializer.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.lang.reflect.Method; +import java.util.Map.Entry; +import java.util.Set; + +/** + * De-serializer for methods having a JsonAnySetter annotation. + */ +public class AnySetterDeserializer extends Lifter implements Deserializer { + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param unliftedProps property names that should not be lifted + * @param setter method used to set the item within an object + */ + public AnySetterDeserializer(Gson gson, Set unliftedProps, Method setter) { + super(gson, unliftedProps, setter, setter.getGenericParameterTypes()[1]); + } + + @Override + public void getFromTree(JsonObject source, Object target) { + for (Entry ent : source.entrySet()) { + String name = ent.getKey(); + + if (shouldLift(name)) { + Object value = fromJsonTree(ent.getValue()); + invoke(target, name, value); + } + } + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java new file mode 100644 index 000000000..954d3f4cc --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java @@ -0,0 +1,389 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.JsonParseException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import lombok.Getter; +import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter; +import org.onap.policy.common.gson.annotation.GsonJsonAnySetter; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +/** + * Data populated while walking the hierarchy of a class. + */ +public class ClassWalker { + + public static final String ANY_GETTER_MISMATCH_ERR = + GsonJsonAnyGetter.class.getSimpleName() + " parameter mismatch for: "; + + public static final String ANY_SETTER_MISMATCH_ERR = + GsonJsonAnySetter.class.getSimpleName() + " parameter mismatch for: "; + + public static final String ANY_SETTER_TYPE_ERR = + GsonJsonAnySetter.class.getSimpleName() + " first parameter must be a string: "; + + /** + * Maps an input property name to an item within the class, where item is one of: + * {@link Field}, {@link Method}, or {@code null}. Entries are overwritten as new + * items are added. + */ + private final Map inProps = new HashMap<>(); + + /** + * Maps an output property name to an item within the class, where item is one of: + * {@link Field}, {@link Method}, or {@code null}. Entries are overwritten as new + * items are added. + */ + private final Map outProps = new HashMap<>(); + + /** + * Maps a method name to a "get" method. Used when overriding properties associated + * with a method. + */ + private final Map getters = new HashMap<>(); + + /** + * Maps a method name to a "set" method. Used when overriding properties associated + * with a method. + */ + private final Map setters = new HashMap<>(); + + /** + * Method having {@link GsonJsonAnyGetter} annotation. Overwritten as new "any-getters" + * are identified. + */ + @Getter + private Method anyGetter = null; + + /** + * Method having {@link GsonJsonAnySetter} annotation. Overwritten as new "any-setters" + * are identified. + */ + @Getter + private Method anySetter = null; + + /** + * Gets the names of input properties that are not being ignored. + * + * @return the non-ignored input property names + */ + public List getInNotIgnored() { + return getNonNull(inProps); + } + + /** + * Gets the names of output properties that are not being ignored. + * + * @return the non-ignored output property names + */ + public List getOutNotIgnored() { + return getNonNull(outProps); + } + + /** + * Gets the property names, associated with a non-null value, from a set of + * properties. + * + * @param props set of properties from which to extract the names + * @return the property names having a non-null value + */ + private List getNonNull(Map props) { + List lst = new ArrayList<>(props.size()); + + for (Entry ent : props.entrySet()) { + if (ent.getValue() != null) { + lst.add(ent.getKey()); + } + } + + return lst; + } + + /** + * Gets the input properties whose values are of the given class. + * + * @param clazz class of properties to get + * @return the input properties of the given class + */ + public List getInProps(Class clazz) { + return getProps(clazz, inProps.values()); + } + + /** + * Gets the output properties whose values are of the given class. + * + * @param clazz class of properties to get + * @return the output properties of the given class + */ + public List getOutProps(Class clazz) { + return getProps(clazz, outProps.values()); + } + + /** + * Gets the properties whose values are of the given class. + * + * @param clazz class of properties to get + * @param values values from which to select + * @return the output properties of the given class + */ + @SuppressWarnings("unchecked") + private List getProps(Class clazz, Collection values) { + List lst = new ArrayList<>(values.size()); + + for (Object val : values) { + if (val != null && val.getClass() == clazz) { + lst.add((T) val); + } + } + + return lst; + } + + /** + * Recursively walks a class hierarchy, including super classes and interfaces, + * examining each class for various annotations. + * + * @param clazz class whose hierarchy is to be walked + */ + public void walkClassHierarchy(Class clazz) { + if (clazz == Object.class) { + return; + } + + // walk interfaces first + for (Class intfc : clazz.getInterfaces()) { + walkClassHierarchy(intfc); + } + + // walk superclass next, overwriting previous items + Class sup = clazz.getSuperclass(); + if (sup != null) { + walkClassHierarchy(sup); + } + + // finally, examine this class, overwriting previous items + examine(clazz); + } + + /** + * Examines a class for annotations, examining fields and then methods. + * + * @param clazz class to be examined + */ + protected void examine(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + examine(field); + } + + for (Method method : clazz.getDeclaredMethods()) { + examine(method); + } + } + + /** + * Examines a field for annotations. + * + * @param field field to be examined + */ + protected void examine(Field field) { + if (field.isSynthetic()) { + return; + } + + int mod = field.getModifiers(); + + if (Modifier.isStatic(mod)) { + // skip static fields + return; + } + + if (!Modifier.isPublic(mod) && field.getAnnotation(GsonJsonProperty.class) == null) { + // private/protected - skip it unless explicitly exposed + return; + } + + if (Modifier.isTransient(mod) && field.getAnnotation(GsonJsonProperty.class) == null) { + // transient - skip it unless explicitly exposed + return; + } + + String name = detmPropName(field); + if (name == null) { + // invalid name + return; + } + + // if ignoring, then insert null into the map, otherwise insert the field + Field annotField = (field.getAnnotation(GsonJsonIgnore.class) != null ? null : field); + + // a field can be both an input and an output + + inProps.put(name, annotField); + outProps.put(name, annotField); + } + + /** + * Examines a method for annotations. + * + * @param method method to be examined + */ + protected void examine(Method method) { + if (method.isSynthetic()) { + return; + } + + int mod = method.getModifiers(); + + if (Modifier.isStatic(mod)) { + // static methods are not exposed + return; + } + + GsonJsonProperty prop = method.getAnnotation(GsonJsonProperty.class); + GsonJsonAnyGetter get = method.getAnnotation(GsonJsonAnyGetter.class); + GsonJsonAnySetter set = method.getAnnotation(GsonJsonAnySetter.class); + + if (!Modifier.isPublic(mod) && prop == null && get == null && set == null) { + // private/protected methods are not exposed, unless annotated + return; + } + + + if (method.getReturnType() == void.class) { + // "void" return type - must be a "setter" method + if (set == null) { + examineSetter(method); + + } else { + examineAnySetter(method); + } + + } else { + // must be a "getter" method + if (get == null) { + examineGetter(method); + + } else { + examineAnyGetter(method); + } + } + } + + /** + * Examines a "setter" method. + * + * @param method method to be examined + */ + private void examineSetter(Method method) { + String name = Adapter.detmSetterPropName(method); + if (name != null && method.getParameterCount() == 1) { + // remove old name mapping, if any + Method old = setters.get(method.getName()); + if (old != null) { + inProps.remove(Adapter.detmSetterPropName(old)); + } + + setters.put(method.getName(), method); + + // if ignoring, then insert null into the map, otherwise insert the method + inProps.put(name, (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method)); + } + } + + /** + * Examines a "getter" method. + * + * @param method method to be examined + */ + private void examineGetter(Method method) { + String name = Adapter.detmGetterPropName(method); + if (name != null && method.getParameterCount() == 0) { + // remove old name mapping, if any + Method old = getters.get(method.getName()); + if (old != null) { + outProps.remove(Adapter.detmGetterPropName(old)); + } + + getters.put(method.getName(), method); + + // if ignoring, then insert null into the map, otherwise insert the method + outProps.put(name, (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method)); + } + } + + /** + * Examines a method having a {@link GsonJsonAnySetter} annotation. + * + * @param method method to be examined + */ + private void examineAnySetter(Method method) { + if (method.getParameterCount() != 2) { + throw new JsonParseException(ANY_SETTER_MISMATCH_ERR + getFqdn(method)); + } + + if (method.getParameterTypes()[0] != String.class) { + throw new JsonParseException(ANY_SETTER_TYPE_ERR + getFqdn(method)); + } + + // if ignoring, then use null, otherwise use the method + anySetter = (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method); + } + + /** + * Examines a method having a {@link GsonJsonAnyGetter} annotation. + * + * @param method method to be examined + */ + private void examineAnyGetter(Method method) { + if (method.getParameterCount() != 0) { + throw new JsonParseException(ANY_GETTER_MISMATCH_ERR + getFqdn(method)); + } + + // if ignoring, then use null, otherwise use the method + anyGetter = (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method); + } + + /** + * Gets the fully qualified name of a method. + * + * @param method method whose name is desired + * @return the fully qualified method name + */ + private String getFqdn(Method method) { + return (method.getDeclaringClass().getName() + "." + method.getName()); + } + + // these may be overridden by junit tests + + protected String detmPropName(Field field) { + return Adapter.detmPropName(field); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java new file mode 100644 index 000000000..4bf6e0c39 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.JsonObject; + +/** + * Super class of all de-serializers. + */ +@FunctionalInterface +public interface Deserializer { + + /** + * Gets an value from a tree, converts it, and puts it into a target object. + * + * @param source tree from which to get the value + * @param target where to place the converted value + */ + void getFromTree(JsonObject source, Object target); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldDeserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldDeserializer.java new file mode 100644 index 000000000..123b01959 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldDeserializer.java @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.lang.reflect.Field; + +/** + * De-serializer for fields that are exposed. + */ +public class FieldDeserializer extends Adapter implements Deserializer { + + public static final String SET_ERR = "cannot set field: "; + + /** + * Field within the object. + */ + private final Field field; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param field field within the object + */ + public FieldDeserializer(Gson gson, Field field) { + super(gson, field); + + this.field = field; + + /* + * Turning off sonar, as this is required for emulation of "jackson". + */ + field.setAccessible(true); // NOSONAR + } + + @Override + public void getFromTree(JsonObject source, Object target) { + JsonElement jsonEl = source.get(getPropName()); + if (jsonEl == null || jsonEl.isJsonNull()) { + return; + } + + Object value = fromJsonTree(jsonEl); + + try { + /* + * Turning off sonar, as this is required for emulation of "jackson". + */ + field.set(target, value); // NOSONAR + + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new JsonParseException(makeError(SET_ERR), e); + } + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldSerializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldSerializer.java new file mode 100644 index 000000000..348ef5a08 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldSerializer.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.lang.reflect.Field; + +/** + * Serializer for fields that are exposed. + */ +public class FieldSerializer extends Adapter implements Serializer { + + public static final String GET_ERR = "cannot get field: "; + + /** + * Field within the object. + */ + private final Field field; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param field field within the object + */ + public FieldSerializer(Gson gson, Field field) { + super(gson, field); + + this.field = field; + + /* + * Turning off sonar, as this is required for emulation of "jackson". + */ + field.setAccessible(true); // NOSONAR + } + + @Override + public void addToTree(Object source, JsonObject target) { + Object value; + try { + value = getFromObject(source); + + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new JsonParseException(makeError(GET_ERR), e); + } + + JsonElement jsonEl = (value == null ? JsonNull.INSTANCE : toJsonTree(value)); + target.add(getPropName(), jsonEl); + } + + /** + * Gets the field from the source object. Overridden during junit testing. + * + * @param source object containing the field + * @return the field's value + * @throws IllegalAccessException if an error occurs + */ + protected Object getFromObject(Object source) throws IllegalAccessException { + return field.get(source); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/JacksonTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/JacksonTypeAdapter.java new file mode 100644 index 000000000..34d61f472 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/JacksonTypeAdapter.java @@ -0,0 +1,107 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.List; + + +/** + * Adapter for a single class that implements a jackson-style behavior. + * + * @param type of class on which the adapter works + */ +public class JacksonTypeAdapter extends TypeAdapter { + + /** + * Used to create an object of the given class. + */ + private final TypeAdapter delegate; + + /** + * Used to serialize/deserialize a JsonElement. + */ + private final TypeAdapter elementAdapter; + + /** + * Serializers for each item within the object. + */ + private final Serializer[] serializers; + + /** + * Deserializers for each item within the object. + */ + private final Deserializer[] deserializers; + + /** + * Constructs the object. + * + * @param gson the associated gson object + * @param delegate default constructor for the type + * @param serializers the serializers to use to serialize items within the object + * @param deserializers the deserializers to use to deserialize items into the object + */ + public JacksonTypeAdapter(Gson gson, TypeAdapter delegate, List serializers, + List deserializers) { + this.delegate = delegate; + this.elementAdapter = gson.getAdapter(JsonElement.class); + this.serializers = serializers.toArray(new Serializer[0]); + this.deserializers = deserializers.toArray(new Deserializer[0]); + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + JsonElement tree = delegate.toJsonTree(value); + + if (tree.isJsonObject()) { + var jsonObj = tree.getAsJsonObject(); + + // serialize each item from the value into the target tree + for (Serializer serializer : serializers) { + serializer.addToTree(value, jsonObj); + } + } + + elementAdapter.write(out, tree); + } + + @Override + public T read(JsonReader in) throws IOException { + JsonElement tree = elementAdapter.read(in); + var object = delegate.fromJsonTree(tree); + + if (tree.isJsonObject()) { + var jsonObj = tree.getAsJsonObject(); + + // deserialize each item from the tree into the target object + for (Deserializer dser : deserializers) { + dser.getFromTree(jsonObj, object); + } + } + + return object; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Lifter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Lifter.java new file mode 100644 index 000000000..bb8ec321e --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Lifter.java @@ -0,0 +1,62 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Set; + +/** + * Super class of serializers and de-serializers that deal with "lifted" data, that is, + * data that is lifted from a nested json object into the containing object. + */ +public class Lifter extends MethodAdapter { + + /** + * Names of the properties that are not to be lifted. + */ + private final Set unliftedProps; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param unliftedProps property names that should not be lifted + * @param accessor method used to access the item from within an object + * @param type the class of value on which this operates + */ + public Lifter(Gson gson, Set unliftedProps, Method accessor, Type type) { + super(gson, accessor, type); + + this.unliftedProps = unliftedProps; + } + + /** + * Determines if a property should be lifted. + * + * @param propName the name of the property + * @return {@code true} if the property should be lifted, {@code false} otherwise + */ + public boolean shouldLift(String propName) { + return !unliftedProps.contains(propName); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodAdapter.java new file mode 100644 index 000000000..579864da4 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodAdapter.java @@ -0,0 +1,69 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * Super class of adapters used to serialize and de-serialize a method. + */ +public class MethodAdapter extends Adapter { + + public static final String INVOKE_ERR = "cannot invoke method to serialize/deserialize: "; + + /** + * Method used to access the item within an object. + */ + private final Method accessor; + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param accessor method used to access the item from within an object + * @param type the class of value on which this operates + */ + public MethodAdapter(Gson gson, Method accessor, Type type) { + super(gson, accessor, type); + + this.accessor = accessor; + } + + /** + * Invokes the accessor method. + * + * @param self object on which to invoke the method + * @param args arguments to be passed to the method + * @return the method's result + */ + public Object invoke(Object self, Object... args) { + try { + return accessor.invoke(self, args); + + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new JsonParseException(makeError(INVOKE_ERR), e); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodDeserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodDeserializer.java new file mode 100644 index 000000000..bb0724eb9 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodDeserializer.java @@ -0,0 +1,53 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.lang.reflect.Method; + +/** + * De-serializer for methods that are exposed. + */ +public class MethodDeserializer extends MethodAdapter implements Deserializer { + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param setter method used to set the item within an object + */ + public MethodDeserializer(Gson gson, Method setter) { + super(gson, setter, setter.getGenericParameterTypes()[0]); + } + + @Override + public void getFromTree(JsonObject source, Object target) { + JsonElement jsonEl = source.get(getPropName()); + if (jsonEl == null || jsonEl.isJsonNull()) { + return; + } + + invoke(target, fromJsonTree(jsonEl)); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodSerializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodSerializer.java new file mode 100644 index 000000000..ab544614f --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodSerializer.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.lang.reflect.Method; + +/** + * Serializer for methods that are exposed. + */ +public class MethodSerializer extends MethodAdapter implements Serializer { + + /** + * Constructs the object. + * + * @param gson Gson object providing type adapters + * @param getter method used to get the item from within an object + */ + public MethodSerializer(Gson gson, Method getter) { + super(gson, getter, getter.getGenericReturnType()); + } + + @Override + public void addToTree(Object source, JsonObject target) { + Object value = invoke(source); + target.add(getPropName(), (value == null ? null : toJsonTree(value))); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Serializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Serializer.java new file mode 100644 index 000000000..254a5fe72 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Serializer.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.JsonObject; + +/** + * Super class of all serializers. + */ +@FunctionalInterface +public interface Serializer { + + /** + * Converts an object and then adds it to a tree. + * + * @param source object to be converted + * @param target tree into which to place the converted object + */ + void addToTree(Object source, JsonObject target); + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/Topic.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/Topic.java new file mode 100644 index 000000000..48dbb7159 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/Topic.java @@ -0,0 +1,90 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019 Samsung Electronics Co., Ltd. + * Copyright (C) 2022,2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.util.List; +import org.onap.policy.common.capabilities.Lockable; +import org.onap.policy.common.capabilities.Startable; + + +/** + * Essential Topic Data. + */ +public interface Topic extends TopicRegisterable, Startable, Lockable { + + /** + * Underlying Communication infrastructure Types. + */ + enum CommInfrastructure { + /** + * KAFKA Communication Infrastructure. + */ + KAFKA, + /** + * NOOP for internal use only. + */ + NOOP, + /** + * REST Communication Infrastructure. + */ + REST + } + + /** + * Gets the canonical topic name. + * + * @return topic name + */ + String getTopic(); + + /** + * Gets the effective topic that is used in + * the network communication. This name is usually + * the topic name. + * + * @return topic name alias + */ + String getEffectiveTopic(); + + /** + * Gets the communication infrastructure type. + * + * @return CommInfrastructure object + */ + CommInfrastructure getTopicCommInfrastructure(); + + /** + * Return list of servers. + * + * @return bus servers + */ + List getServers(); + + /** + * Get the more recent events in this topic entity. + * + * @return array of most recent events + */ + String[] getRecentEvents(); + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java new file mode 100644 index 000000000..5511a82cc --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java @@ -0,0 +1,238 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2022,2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.capabilities.Lockable; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSink; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSource; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSink; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSource; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; + +/** + * Abstraction to manage the system's Networked Topic Endpoints, sources of all events input into + * the System. + */ +public interface TopicEndpoint extends Startable, Lockable { + + /** + * Add topics configuration (sources and sinks) into a single list. + * + * @param properties topic configuration + * @return topic list + * @throws IllegalArgumentException when invalid arguments are provided + */ + List addTopics(Properties properties); + + /** + * Add topics configuration (sources and sinks) into a single list. + * + * @param params parameters to configure topic + * @return topic list + * @throws IllegalArgumentException when invalid arguments are provided + */ + List addTopics(TopicParameterGroup params); + + /** + * Add Topic Sources to the communication infrastructure initialized per properties. + * + * @param properties properties for Topic Source construction + * @return a list of generic Topic Sources + * @throws IllegalArgumentException when invalid arguments are provided + */ + List addTopicSources(Properties properties); + + + /** + * Add Topic Sources to the communication infrastructure initialized per properties. + * + * @param paramList parameters for Topic Source construction + * @return a list of generic Topic Sources + * @throws IllegalArgumentException when invalid arguments are provided + */ + List addTopicSources(List paramList); + + /** + * Add Topic Sinks to the communication infrastructure initialized per properties. + * + * @param properties properties for Topic Sink construction + * @return a list of generic Topic Sinks + * @throws IllegalArgumentException when invalid arguments are provided + */ + List addTopicSinks(Properties properties); + + /** + * Add Topic Sinks to the communication infrastructure initialized per properties. + * + * @param paramList parameters for Topic Sink construction + * @return a list of generic Topic Sinks + * @throws IllegalArgumentException when invalid arguments are provided + */ + List addTopicSinks(List paramList); + + /** + * Gets all Topic Sources. + * + * @return the Topic Source List + */ + List getTopicSources(); + + /** + * Get the Topic Sources for the given topic name. + * + * @param topicNames the topic name + * + * @return the Topic Source List + * @throws IllegalStateException if the entity is in an invalid state + * @throws IllegalArgumentException if invalid parameters are present + */ + List getTopicSources(List topicNames); + + /** + * Gets the Topic Source for the given topic name and underlying communication infrastructure + * type. + * + * @param commType communication infrastructure type + * @param topicName the topic name + * + * @return the Topic Source + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicReaders for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + * @throws UnsupportedOperationException if the operation is not supported. + */ + TopicSource getTopicSource(Topic.CommInfrastructure commType, String topicName); + + /** + * Get the Noop Source for the given topic name. + * + * @param topicName the topic name. + * @return the Noop Source. + */ + NoopTopicSource getNoopTopicSource(String topicName); + + /** + * Get the Kafka Source for the given topic name. + * + * @param topicName the topic name. + * @return the Kafka Source. + */ + KafkaTopicSource getKafkaTopicSource(String topicName); + + /** + * Get the Topic Sinks for the given topic name. + * + * @param topicNames the topic names + * @return the Topic Sink List + */ + List getTopicSinks(List topicNames); + + /** + * Get the Topic Sinks for the given topic name and all the underlying communication + * infrastructure type. + * + * @param topicName the topic name + * + * @return the Topic Sink List + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicWriters for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + List getTopicSinks(String topicName); + + /** + * Gets all Topic Sinks. + * + * @return the Topic Sink List + */ + List getTopicSinks(); + + /** + * Get the Topic Sinks for the given topic name and underlying communication infrastructure type. + * + * @param topicName the topic name + * @param commType communication infrastructure type + * + * @return the Topic Sink List + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicWriters for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + TopicSink getTopicSink(Topic.CommInfrastructure commType, String topicName); + + /** + * Get the no-op Topic Sink for the given topic name. + * + * @param topicName the topic name + * + * @return the Topic Source + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicReaders for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + NoopTopicSink getNoopTopicSink(String topicName); + + /** + * Get the KAFKA Topic Source for the given topic name. + * + * @param topicName the topic name + * + * @return the Topic Source + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicReaders for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + KafkaTopicSink getKafkaTopicSink(String topicName); + + /** + * Gets only the KAFKA Topic Sources. + * + * @return the KAFKA Topic Source List + */ + List getKafkaTopicSources(); + + /** + * Gets only the NOOP Topic Sources. + * + * @return the NOOP Topic Source List + */ + List getNoopTopicSources(); + + /** + * Gets only the KAFKA Topic Sinks. + * + * @return the KAFKA Topic Sinks List + */ + List getKafkaTopicSinks(); + + /** + * Gets only the NOOP Topic Sinks. + * + * @return the NOOP Topic Sinks List + */ + List getNoopTopicSinks(); + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java new file mode 100644 index 000000000..40b9c2354 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TopicEndpointManager { + + /** + * Topic endpoint manager. + */ + @Getter + static TopicEndpoint manager = new TopicEndpointProxy(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java new file mode 100644 index 000000000..9dbf54181 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java @@ -0,0 +1,485 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import lombok.Getter; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicFactories; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSink; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSource; +import org.onap.policy.common.message.bus.event.noop.NoopTopicFactories; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSink; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSource; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation of the Topic Endpoint Manager, proxies operations to the appropriate + * implementation(s). + */ +@Getter +public class TopicEndpointProxy implements TopicEndpoint { + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(TopicEndpointProxy.class); + + /** + * Is this element locked boolean. + */ + private volatile boolean locked = false; + + /** + * Is this element alive boolean. + */ + private volatile boolean alive = false; + + @Override + public List addTopics(Properties properties) { + List topics = new ArrayList<>(addTopicSources(properties)); + topics.addAll(addTopicSinks(properties)); + return topics; + } + + @Override + public List addTopics(TopicParameterGroup params) { + List sinks = + (params.getTopicSinks() != null ? params.getTopicSinks() : Collections.emptyList()); + List sources = + (params.getTopicSources() != null ? params.getTopicSources() : Collections.emptyList()); + + List topics = new ArrayList<>(sinks.size() + sources.size()); + topics.addAll(addTopicSources(sources)); + topics.addAll(addTopicSinks(sinks)); + return topics; + } + + @Override + public List addTopicSources(List paramList) { + List sources = new ArrayList<>(paramList.size()); + + for (TopicParameters param : paramList) { + switch (Topic.CommInfrastructure.valueOf(param.getTopicCommInfrastructure().toUpperCase())) { + case KAFKA: + sources.add(KafkaTopicFactories.getSourceFactory().build(param)); + break; + case NOOP: + sources.add(NoopTopicFactories.getSourceFactory().build(param)); + break; + default: + logger.debug("Unknown source type {} for topic: {}", param.getTopicCommInfrastructure(), + param.getTopic()); + break; + } + } + + lockSources(sources); + + return sources; + } + + @Override + public List addTopicSources(Properties properties) { + + // 1. Create KAFKA Sources + // 2. Create NOOP Sources + + List sources = new ArrayList<>(); + + sources.addAll(KafkaTopicFactories.getSourceFactory().build(properties)); + sources.addAll(NoopTopicFactories.getSourceFactory().build(properties)); + + lockSources(sources); + + return sources; + } + + private void lockSources(List sources) { + if (this.isLocked()) { + sources.forEach(TopicSource::lock); + } + } + + @Override + public List addTopicSinks(List paramList) { + List sinks = new ArrayList<>(paramList.size()); + + for (TopicParameters param : paramList) { + switch (Topic.CommInfrastructure.valueOf(param.getTopicCommInfrastructure().toUpperCase())) { + case KAFKA: + sinks.add(KafkaTopicFactories.getSinkFactory().build(param)); + break; + case NOOP: + sinks.add(NoopTopicFactories.getSinkFactory().build(param)); + break; + default: + logger.debug("Unknown sink type {} for topic: {}", param.getTopicCommInfrastructure(), + param.getTopic()); + break; + } + } + + lockSinks(sinks); + + return sinks; + } + + @Override + public List addTopicSinks(Properties properties) { + // 1. Create KAFKA Sinks + // 2. Create NOOP Sinks + + final List sinks = new ArrayList<>(); + + sinks.addAll(KafkaTopicFactories.getSinkFactory().build(properties)); + sinks.addAll(NoopTopicFactories.getSinkFactory().build(properties)); + + lockSinks(sinks); + + return sinks; + } + + private void lockSinks(List sinks) { + if (this.isLocked()) { + sinks.forEach(TopicSink::lock); + } + } + + @Override + public List getTopicSources() { + + final List sources = new ArrayList<>(); + + sources.addAll(KafkaTopicFactories.getSourceFactory().inventory()); + sources.addAll(NoopTopicFactories.getSourceFactory().inventory()); + + return sources; + } + + @Override + public List getTopicSources(List topicNames) { + + if (topicNames == null) { + throw new IllegalArgumentException("must provide a list of topics"); + } + + final List sources = new ArrayList<>(); + + topicNames.forEach(topic -> { + try { + sources.add(Objects.requireNonNull(this.getKafkaTopicSource(topic))); + } catch (final Exception e) { + logger.debug("No KAFKA source for topic: {}", topic, e); + } + + try { + sources.add(Objects.requireNonNull(this.getNoopTopicSource(topic))); + } catch (final Exception e) { + logger.debug("No NOOP source for topic: {}", topic, e); + } + }); + + return sources; + } + + @Override + public List getTopicSinks() { + + final List sinks = new ArrayList<>(); + + sinks.addAll(KafkaTopicFactories.getSinkFactory().inventory()); + sinks.addAll(NoopTopicFactories.getSinkFactory().inventory()); + + return sinks; + } + + @Override + public List getTopicSinks(List topicNames) { + + if (topicNames == null) { + throw new IllegalArgumentException("must provide a list of topics"); + } + + final List sinks = new ArrayList<>(); + for (final String topic : topicNames) { + try { + sinks.add(Objects.requireNonNull(this.getKafkaTopicSink(topic))); + } catch (final Exception e) { + logger.debug("No KAFKA sink for topic: {}", topic, e); + } + + try { + sinks.add(Objects.requireNonNull(this.getNoopTopicSink(topic))); + } catch (final Exception e) { + logger.debug("No NOOP sink for topic: {}", topic, e); + } + } + return sinks; + } + + @Override + public List getTopicSinks(String topicName) { + if (topicName == null) { + throw paramException(null); + } + + final List sinks = new ArrayList<>(); + + try { + sinks.add(this.getKafkaTopicSink(topicName)); + } catch (final Exception e) { + logNoSink(topicName, e); + } + + try { + sinks.add(this.getNoopTopicSink(topicName)); + } catch (final Exception e) { + logNoSink(topicName, e); + } + + return sinks; + } + + @GsonJsonIgnore + @Override + public List getKafkaTopicSources() { + return KafkaTopicFactories.getSourceFactory().inventory(); + } + + @GsonJsonIgnore + @Override + public List getNoopTopicSources() { + return NoopTopicFactories.getSourceFactory().inventory(); + } + + @Override + @GsonJsonIgnore + public List getKafkaTopicSinks() { + return KafkaTopicFactories.getSinkFactory().inventory(); + } + + @GsonJsonIgnore + @Override + public List getNoopTopicSinks() { + return NoopTopicFactories.getSinkFactory().inventory(); + } + + @Override + public boolean start() { + + synchronized (this) { + if (this.locked) { + throw new IllegalStateException(this + " is locked"); + } + + if (this.alive) { + return true; + } + + this.alive = true; + } + + final List endpoints = this.getEndpoints(); + + var success = true; + for (final Startable endpoint : endpoints) { + try { + success = endpoint.start() && success; + } catch (final Exception e) { + success = false; + logger.error("Problem starting endpoint: {}", endpoint, e); + } + } + + return success; + } + + @Override + public boolean stop() { + + /* + * stop regardless if it is locked, in other words, stop operation has precedence over + * locks. + */ + synchronized (this) { + this.alive = false; + } + + final List endpoints = this.getEndpoints(); + + var success = true; + for (final Startable endpoint : endpoints) { + try { + success = endpoint.stop() && success; + } catch (final Exception e) { + success = false; + logger.error("Problem stopping endpoint: {}", endpoint, e); + } + } + + return success; + } + + /** + * Gets the endpoints. + * + * @return list of managed endpoints + */ + @GsonJsonIgnore + protected List getEndpoints() { + final List endpoints = new ArrayList<>(); + + endpoints.addAll(this.getTopicSources()); + endpoints.addAll(this.getTopicSinks()); + + return endpoints; + } + + @Override + public void shutdown() { + this.stop(); + + KafkaTopicFactories.getSourceFactory().destroy(); + KafkaTopicFactories.getSinkFactory().destroy(); + + NoopTopicFactories.getSinkFactory().destroy(); + NoopTopicFactories.getSourceFactory().destroy(); + + } + + @Override + public boolean lock() { + boolean shouldLock; + + synchronized (this) { + shouldLock = !this.locked; + this.locked = true; + } + + if (shouldLock) { + for (final TopicSource source : this.getTopicSources()) { + source.lock(); + } + + for (final TopicSink sink : this.getTopicSinks()) { + sink.lock(); + } + } + + return true; + } + + @Override + public boolean unlock() { + boolean shouldUnlock; + + synchronized (this) { + shouldUnlock = this.locked; + this.locked = false; + } + + if (shouldUnlock) { + for (final TopicSource source : this.getTopicSources()) { + source.unlock(); + } + + for (final TopicSink sink : this.getTopicSinks()) { + sink.unlock(); + } + } + + return true; + } + + @Override + public TopicSource getTopicSource(Topic.CommInfrastructure commType, String topicName) { + + if (commType == null) { + throw paramException(topicName); + } + + if (topicName == null) { + throw paramException(null); + } + + return switch (commType) { + case KAFKA -> this.getKafkaTopicSource(topicName); + case NOOP -> this.getNoopTopicSource(topicName); + default -> throw new UnsupportedOperationException("Unsupported " + commType.name()); + }; + } + + @Override + public TopicSink getTopicSink(Topic.CommInfrastructure commType, String topicName) { + if (commType == null) { + throw paramException(topicName); + } + + if (topicName == null) { + throw paramException(null); + } + + return switch (commType) { + case KAFKA -> this.getKafkaTopicSink(topicName); + case NOOP -> this.getNoopTopicSink(topicName); + default -> throw new UnsupportedOperationException("Unsupported " + commType.name()); + }; + } + + @Override + public KafkaTopicSource getKafkaTopicSource(String topicName) { + return KafkaTopicFactories.getSourceFactory().get(topicName); + } + + @Override + public NoopTopicSource getNoopTopicSource(String topicName) { + return NoopTopicFactories.getSourceFactory().get(topicName); + } + + @Override + public KafkaTopicSink getKafkaTopicSink(String topicName) { + return KafkaTopicFactories.getSinkFactory().get(topicName); + } + + @Override + public NoopTopicSink getNoopTopicSink(String topicName) { + return NoopTopicFactories.getSinkFactory().get(topicName); + } + + private IllegalArgumentException paramException(String topicName) { + return new IllegalArgumentException( + "Invalid parameter: a communication infrastructure required to fetch " + topicName); + } + + private void logNoSink(String topicName, Exception ex) { + logger.debug("No sink for topic: {}", topicName, ex); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java new file mode 100644 index 000000000..f98962433 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java @@ -0,0 +1,37 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Listener for event messages entering the Policy Engine. + */ +@FunctionalInterface +public interface TopicListener { + + /** + * Notification of a new Event over a given Topic. + * + * @param commType communication infrastructure type + * @param topic topic name + * @param event event message as a string + */ + void onTopicEvent(Topic.CommInfrastructure commType, String topic, String event); + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java new file mode 100644 index 000000000..1d6873e9d --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java @@ -0,0 +1,41 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Marks a Topic entity as registerable. + */ +public interface TopicRegisterable { + + /** + * Register for notification of events with this Topic Entity. + * + * @param topicListener the listener of events + */ + void register(TopicListener topicListener); + + /** + * Unregisters for notification of events with this Topic Entity. + * + * @param topicListener the listener of events + */ + void unregister(TopicListener topicListener); + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java new file mode 100644 index 000000000..d5269b9a6 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Marks a given Topic Endpoint as able to send messages over a topic. + */ +public interface TopicSink extends Topic { + + /** + * Sends a string message over this Topic Endpoint. + * + * @param message message to send + * @return true if the send operation succeeded, false otherwise + * @throws IllegalArgumentException an invalid message has been provided + * @throws IllegalStateException the entity is in a state that prevents + * it from sending messages, for example, locked or stopped. + */ + boolean send(String message); + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java new file mode 100644 index 000000000..f0dc3b748 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Marker for a Topic Entity, indicating that the entity is able to read + * over a topic. + */ +public interface TopicSource extends Topic { + + /** + * Pushes an event into the source programmatically. + * + * @param event the event in json format + * @return true if it can be processed correctly, false otherwise + */ + boolean offer(String event); + +} \ No newline at end of file diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java new file mode 100644 index 000000000..360a88a2b --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java @@ -0,0 +1,39 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +/** + * API. + */ +public interface ApiKeyEnabled { + /** + * Get API key. + * + * @return api key + */ + String getApiKey(); + + /** + * Get API secret. + * + * @return api secret + */ + String getApiSecret(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java new file mode 100644 index 000000000..925949aaf --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java @@ -0,0 +1,278 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020,2023 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingConsumerInterceptor; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.header.Headers; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapper around libraries to consume from message bus. + */ +public interface BusConsumer { + + /** + * fetch messages. + * + * @return list of messages + * @throws IOException when error encountered by underlying libraries + */ + Iterable fetch() throws IOException; + + /** + * close underlying library consumer. + */ + void close(); + + /** + * Consumer that handles fetch() failures by sleeping. + */ + abstract class FetchingBusConsumer implements BusConsumer { + private static final Logger logger = LoggerFactory.getLogger(FetchingBusConsumer.class); + + /** + * Fetch timeout. + */ + protected int fetchTimeout; + + /** + * Time to sleep on a fetch failure. + */ + @Getter + private final int sleepTime; + + /** + * Counted down when {@link #close()} is invoked. + */ + private final CountDownLatch closeCondition = new CountDownLatch(1); + + + /** + * Constructs the object. + * + * @param busTopicParams parameters for the bus topic + */ + protected FetchingBusConsumer(BusTopicParams busTopicParams) { + this.fetchTimeout = busTopicParams.getFetchTimeout(); + + if (this.fetchTimeout <= 0) { + this.sleepTime = DEFAULT_TIMEOUT_MS_FETCH; + } else { + // don't sleep too long, even if fetch timeout is large + this.sleepTime = Math.min(this.fetchTimeout, DEFAULT_TIMEOUT_MS_FETCH); + } + } + + /** + * Causes the thread to sleep; invoked after fetch() fails. If the consumer is closed, + * or the thread is interrupted, then this will return immediately. + */ + protected void sleepAfterFetchFailure() { + try { + logger.info("{}: backoff for {}ms", this, sleepTime); + if (this.closeCondition.await(this.sleepTime, TimeUnit.MILLISECONDS)) { + logger.info("{}: closed while handling fetch error", this); + } + + } catch (InterruptedException e) { + logger.warn("{}: interrupted while handling fetch error", this, e); + Thread.currentThread().interrupt(); + } + } + + @Override + public void close() { + this.closeCondition.countDown(); + } + } + + /** + * Kafka based consumer. + */ + class KafkaConsumerWrapper extends FetchingBusConsumer { + + /** + * logger. + */ + private static final Logger logger = LoggerFactory.getLogger(KafkaConsumerWrapper.class); + + private static final String KEY_DESERIALIZER = "org.apache.kafka.common.serialization.StringDeserializer"; + + /** + * Kafka consumer. + */ + protected KafkaConsumer consumer; + protected Properties kafkaProps; + + protected boolean allowTracing; + + /** + * Kafka Consumer Wrapper. + * BusTopicParam - object contains the following parameters + * servers - messaging bus hosts. + * topic - topic + * + * @param busTopicParams - The parameters for the bus topic + */ + public KafkaConsumerWrapper(BusTopicParams busTopicParams) { + super(busTopicParams); + + if (busTopicParams.isTopicInvalid()) { + throw new IllegalArgumentException("No topic for Kafka"); + } + + //Setup Properties for consumer + kafkaProps = new Properties(); + kafkaProps.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + busTopicParams.getServers().get(0)); + + if (busTopicParams.isAdditionalPropsValid()) { + kafkaProps.putAll(busTopicParams.getAdditionalProps()); + } + + if (kafkaProps.get(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, KEY_DESERIALIZER); + } + if (kafkaProps.get(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KEY_DESERIALIZER); + } + if (kafkaProps.get(ConsumerConfig.GROUP_ID_CONFIG) == null) { + kafkaProps.setProperty(ConsumerConfig.GROUP_ID_CONFIG, busTopicParams.getConsumerGroup()); + } + if (busTopicParams.isAllowTracing()) { + this.allowTracing = true; + kafkaProps.setProperty(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, + TracingConsumerInterceptor.class.getName()); + } + + consumer = new KafkaConsumer<>(kafkaProps); + //Subscribe to the topic + consumer.subscribe(List.of(busTopicParams.getTopic())); + } + + @Override + public Iterable fetch() { + ConsumerRecords records = this.consumer.poll(Duration.ofMillis(fetchTimeout)); + if (records == null || records.count() <= 0) { + return Collections.emptyList(); + } + List messages = new ArrayList<>(records.count()); + try { + if (allowTracing) { + createParentTraceContext(records); + } + + for (TopicPartition partition : records.partitions()) { + List> partitionRecords = records.records(partition); + for (ConsumerRecord partitionRecord : partitionRecords) { + messages.add(partitionRecord.value()); + } + long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset(); + consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1))); + } + } catch (Exception e) { + logger.error("{}: cannot fetch, throwing exception after sleep...", this); + sleepAfterFetchFailure(); + throw e; + } + return messages; + } + + private void createParentTraceContext(ConsumerRecords records) { + TraceParentInfo traceParentInfo = new TraceParentInfo(); + for (ConsumerRecord consumerRecord : records) { + + Headers consumerRecordHeaders = consumerRecord.headers(); + traceParentInfo = processTraceParentHeader(consumerRecordHeaders); + } + + SpanContext spanContext = SpanContext.createFromRemoteParent( + traceParentInfo.getTraceId(), traceParentInfo.getSpanId(), + TraceFlags.getSampled(), TraceState.builder().build()); + + Context.current().with(Span.wrap(spanContext)).makeCurrent(); + } + + private TraceParentInfo processTraceParentHeader(Headers headers) { + TraceParentInfo traceParentInfo = new TraceParentInfo(); + if (headers.lastHeader("traceparent") != null) { + traceParentInfo.setParentTraceId(new String(headers.lastHeader( + "traceparent").value(), StandardCharsets.UTF_8)); + + String[] parts = traceParentInfo.getParentTraceId().split("-"); + traceParentInfo.setTraceId(parts[1]); + traceParentInfo.setSpanId(parts[2]); + } + + return traceParentInfo; + } + + @Data + @NoArgsConstructor + private static class TraceParentInfo { + private String parentTraceId; + private String traceId; + private String spanId; + } + + @Override + public void close() { + super.close(); + this.consumer.close(); + logger.info("Kafka Consumer exited {}", this); + } + + @Override + public String toString() { + return "KafkaConsumerWrapper [fetchTimeout=" + fetchTimeout + "]"; + } + } +} + + diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java new file mode 100644 index 000000000..10c7db2df --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java @@ -0,0 +1,43 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020,2023 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +public interface BusPublisher { + + String NO_MESSAGE_PROVIDED = "No message provided"; + String LOG_CLOSE = "{}: CLOSE"; + + /** + * sends a message. + * + * @param partitionId id + * @param message the message + * @return true if success, false otherwise + * @throws IllegalArgumentException if no message provided + */ + boolean send(String partitionId, String message); + + /** + * closes the publisher. + */ + void close(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java new file mode 100644 index 000000000..6516945a2 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java @@ -0,0 +1,109 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import lombok.Getter; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Bus Topic Base. + */ +@Getter +public abstract class BusTopicBase extends TopicBase implements ApiKeyEnabled { + + /** + * API Key. + */ + protected String apiKey; + + /** + * API Secret. + */ + protected String apiSecret; + + /** + * Use https. + */ + protected boolean useHttps; + + /** + * Allow tracing. + */ + protected boolean allowTracing; + + /** + * allow self-signed certificates. + */ + protected boolean allowSelfSignedCerts; + + /** + * Instantiates a new Bus Topic Base. + * + *

servers list of servers + * topic: the topic name + * apiKey: API Key + * apiSecret: API Secret + * useHttps: does connection use HTTPS? + * allowTracing: Is tracing allowed? + * allowSelfSignedCerts: are self-signed certificates allow + * + * @param busTopicParams holds all our parameters + * @throws IllegalArgumentException if invalid parameters are present + */ + protected BusTopicBase(BusTopicParams busTopicParams) { + super(busTopicParams.getServers(), busTopicParams.getTopic(), busTopicParams.getEffectiveTopic()); + this.apiKey = busTopicParams.getApiKey(); + this.apiSecret = busTopicParams.getApiSecret(); + this.useHttps = busTopicParams.isUseHttps(); + this.allowTracing = busTopicParams.isAllowTracing(); + this.allowSelfSignedCerts = busTopicParams.isAllowSelfSignedCerts(); + } + + protected boolean anyNullOrEmpty(String... args) { + for (String arg : args) { + if (arg == null || arg.isEmpty()) { + return true; + } + } + + return false; + } + + protected boolean allNullOrEmpty(String... args) { + for (String arg : args) { + if (!(arg == null || arg.isEmpty())) { + return false; + } + } + + return true; + } + + + @Override + public String toString() { + return "BusTopicBase [apiKey=" + apiKey + ", apiSecret=" + apiSecret + ", useHttps=" + useHttps + + ", allowSelfSignedCerts=" + allowSelfSignedCerts + ", toString()=" + super.toString() + "]"; + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java new file mode 100644 index 000000000..54b086191 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java @@ -0,0 +1,42 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017, 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import org.onap.policy.common.message.bus.event.TopicSink; + +/** + * Topic Sink over Bus Infrastructure (KAFKA). + */ +public interface BusTopicSink extends ApiKeyEnabled, TopicSink { + + /** + * Sets the partition key for published messages. + * + * @param partitionKey the partition key + */ + void setPartitionKey(String partitionKey); + + /** + * Return the partition key in used by the system to publish messages. + * + * @return the partition key + */ + String getPartitionKey(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java new file mode 100644 index 000000000..974c02bb6 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java @@ -0,0 +1,57 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import org.onap.policy.common.message.bus.event.TopicSource; + +/** + * Generic Topic Source for Bus Communication Infrastructure. + * + */ +public interface BusTopicSource extends ApiKeyEnabled, TopicSource { + + /** + * Gets the consumer group. + * + * @return consumer group + */ + public String getConsumerGroup(); + + /** + * Gets the consumer instance. + * + * @return consumer instance + */ + public String getConsumerInstance(); + + /** + * Gets the fetch timeout. + * + * @return fetch timeout + */ + public int getFetchTimeout(); + + /** + * Gets the fetch limit. + * + * @return fetch limit + */ + public int getFetchLimit(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java new file mode 100644 index 000000000..6a30f00f6 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java @@ -0,0 +1,165 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018-2019 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. +* ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Transport Agnostic Bus Topic Sink to carry out the core functionality to interact with a sink. + * + */ +public abstract class InlineBusTopicSink extends BusTopicBase implements BusTopicSink { + + /** + * Loggers. + */ + private static final Logger logger = LoggerFactory.getLogger(InlineBusTopicSink.class); + + /** + * The partition key to publish to. + */ + @Getter + @Setter + protected String partitionKey; + + /** + * Message bus publisher. + */ + protected BusPublisher publisher; + + /** + * Constructor for abstract sink. + * @param busTopicParams contains below listed attributes + * servers: servers + * topic: topic + * apiKey: api secret + * apiSecret: api secret + * partitionId: partition id + * useHttps: does connection use HTTPS? + * allowTracing: is tracing allowed? + * allowSelfSignedCerts: are self-signed certificates allow * + * @throws IllegalArgumentException if invalid parameters are passed in + */ + protected InlineBusTopicSink(BusTopicParams busTopicParams) { + + super(busTopicParams); + + if (busTopicParams.isPartitionIdInvalid()) { + this.partitionKey = UUID.randomUUID().toString(); + } else { + this.partitionKey = busTopicParams.getPartitionId(); + } + } + + /** + * Initialize the Bus publisher. + */ + public abstract void init(); + + @Override + public boolean start() { + logger.info("{}: starting", this); + + synchronized (this) { + if (!this.alive) { + if (locked) { + throw new IllegalStateException(this + " is locked."); + } + + this.init(); + this.alive = true; + } + } + + return true; + } + + @Override + public boolean stop() { + + BusPublisher publisherCopy; + synchronized (this) { + this.alive = false; + publisherCopy = this.publisher; + this.publisher = null; + } + + if (publisherCopy != null) { + try { + publisherCopy.close(); + } catch (Exception e) { + logger.warn("{}: cannot stop publisher because of {}", this, e.getMessage(), e); + } + } else { + logger.warn("{}: there is no publisher", this); + return false; + } + + return true; + } + + @Override + public boolean send(String message) { + + if (message == null || message.isEmpty()) { + throw new IllegalArgumentException("Message to send is empty"); + } + + if (!this.alive) { + throw new IllegalStateException(this + " is stopped"); + } + + try { + synchronized (this) { + this.recentEvents.add(message); + } + + NetLoggerUtil.log(EventType.OUT, this.getTopicCommInfrastructure(), this.topic, message); + + publisher.send(this.partitionKey, message); + broadcast(message); + } catch (Exception e) { + logger.warn("{}: cannot send because of {}", this, e.getMessage(), e); + return false; + } + + return true; + } + + @Override + public void shutdown() { + this.stop(); + } + + @Override + public String toString() { + return "InlineBusTopicSink [partitionId=" + partitionKey + ", alive=" + alive + ", publisher=" + publisher + + "]"; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java new file mode 100644 index 000000000..912b698c6 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java @@ -0,0 +1,288 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018-2019 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.NO_LIMIT_FETCH; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.NO_TIMEOUT_MS_FETCH; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.UUID; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.network.NetworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This topic source implementation specializes in reading messages over a bus topic source and + * notifying its listeners. + */ +public abstract class SingleThreadedBusTopicSource extends BusTopicBase + implements Runnable, BusTopicSource { + + /** + * Not to be converted to PolicyLogger. This will contain all in/out traffic and only + * that in a single file in a concise format. + */ + private static final Logger logger = LoggerFactory.getLogger(SingleThreadedBusTopicSource.class); + + /** + * Bus consumer group. + */ + @Getter + protected final String consumerGroup; + + /** + * Bus consumer instance. + */ + @Getter + protected final String consumerInstance; + + /** + * Bus fetch timeout. + */ + @Getter + protected final int fetchTimeout; + + /** + * Bus fetch limit. + */ + @Getter + protected final int fetchLimit; + + /** + * Message Bus Consumer. + */ + protected BusConsumer consumer; + + /** + * Independent thread reading message over my topic. + */ + protected Thread busPollerThread; + + + /** + * Constructor. + * + * @param busTopicParams topic parameters + * + * @throws IllegalArgumentException An invalid parameter passed in + */ + protected SingleThreadedBusTopicSource(BusTopicParams busTopicParams) { + + super(busTopicParams); + + if (busTopicParams.isConsumerGroupInvalid() && busTopicParams.isConsumerInstanceInvalid()) { + this.consumerGroup = UUID.randomUUID().toString(); + this.consumerInstance = NetworkUtil.getHostname(); + + } else if (busTopicParams.isConsumerGroupInvalid()) { + this.consumerGroup = UUID.randomUUID().toString(); + this.consumerInstance = busTopicParams.getConsumerInstance(); + + } else if (busTopicParams.isConsumerInstanceInvalid()) { + this.consumerGroup = busTopicParams.getConsumerGroup(); + this.consumerInstance = UUID.randomUUID().toString(); + + } else { + this.consumerGroup = busTopicParams.getConsumerGroup(); + this.consumerInstance = busTopicParams.getConsumerInstance(); + } + + if (busTopicParams.getFetchTimeout() <= 0) { + this.fetchTimeout = NO_TIMEOUT_MS_FETCH; + } else { + this.fetchTimeout = busTopicParams.getFetchTimeout(); + } + + if (busTopicParams.getFetchLimit() <= 0) { + this.fetchLimit = NO_LIMIT_FETCH; + } else { + this.fetchLimit = busTopicParams.getFetchLimit(); + } + + } + + /** + * Initialize the Bus client. + */ + public abstract void init() throws MalformedURLException; + + @Override + public void register(TopicListener topicListener) { + + super.register(topicListener); + + try { + if (!alive && !locked) { + this.start(); + } else { + logger.info("{}: register: start not attempted", this); + } + } catch (Exception e) { + logger.warn("{}: cannot start after registration of because of: {}", this, topicListener, e); + } + } + + @Override + public void unregister(TopicListener topicListener) { + boolean stop; + synchronized (this) { + super.unregister(topicListener); + stop = this.topicListeners.isEmpty(); + } + + if (stop) { + this.stop(); + } + } + + @Override + public boolean start() { + logger.info("{}: starting", this); + + synchronized (this) { + + if (alive) { + return true; + } + + if (locked) { + throw new IllegalStateException(this + " is locked."); + } + + if (this.busPollerThread == null || !this.busPollerThread.isAlive() || this.consumer == null) { + + try { + this.init(); + this.alive = true; + this.busPollerThread = makePollerThread(); + this.busPollerThread.setName(this.getTopicCommInfrastructure() + "-source-" + this.getTopic()); + busPollerThread.start(); + return true; + } catch (Exception e) { + throw new IllegalStateException(this + ": cannot start", e); + } + } + } + + return false; + } + + /** + * Makes a new thread to be used for polling. + * + * @return a new Thread + */ + protected Thread makePollerThread() { + return new Thread(this); + } + + @Override + public boolean stop() { + logger.info("{}: stopping", this); + + synchronized (this) { + BusConsumer consumerCopy = this.consumer; + + this.alive = false; + this.consumer = null; + + if (consumerCopy != null) { + try { + consumerCopy.close(); + } catch (Exception e) { + logger.warn("{}: stop failed because of {}", this, e.getMessage(), e); + } + } + } + + Thread.yield(); + + return true; + } + + /** + * Run thread method for the Bus Reader. + */ + @Override + public void run() { + while (this.alive) { + try { + fetchAllMessages(); + } catch (IOException | RuntimeException e) { + logger.error("{}: cannot fetch", this, e); + } + } + + logger.info("{}: exiting thread", this); + } + + private void fetchAllMessages() throws IOException { + for (String event : this.consumer.fetch()) { + synchronized (this) { + this.recentEvents.add(event); + } + + NetLoggerUtil.log(EventType.IN, this.getTopicCommInfrastructure(), this.topic, event); + + broadcast(event); + + if (!this.alive) { + return; + } + } + } + + @Override + public boolean offer(String event) { + if (!this.alive) { + throw new IllegalStateException(this + " is not alive."); + } + + synchronized (this) { + this.recentEvents.add(event); + } + + NetLoggerUtil.log(EventType.IN, this.getTopicCommInfrastructure(), this.topic, event); + + return broadcast(event); + } + + @Override + public String toString() { + return "SingleThreadedBusTopicSource [consumerGroup=" + consumerGroup + ", consumerInstance=" + consumerInstance + + ", fetchTimeout=" + fetchTimeout + ", fetchLimit=" + fetchLimit + ", consumer=" + this.consumer + + ", alive=" + alive + ", locked=" + locked + ", uebThread=" + busPollerThread + ", topicListeners=" + + topicListeners.size() + ", toString()=" + super.toString() + "]"; + } + + @Override + public void shutdown() { + this.stop(); + this.topicListeners.clear(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java new file mode 100644 index 000000000..4d1fbc9ef --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java @@ -0,0 +1,243 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import org.apache.commons.collections4.queue.CircularFifoQueue; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Getter +public abstract class TopicBase implements Topic { + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(TopicBase.class); + + /** + * List of servers. + */ + protected List servers; + + /** + * Topic. + */ + protected final String topic; + + /** + * Topic Alias. + */ + protected final String effectiveTopic; + + /** + * Event cache. + */ + protected CircularFifoQueue recentEvents = new CircularFifoQueue<>(10); + + /** + * Am I running? reflects invocation of start()/stop() !locked & start() => alive stop() => + * !alive. + */ + protected volatile boolean alive = false; + + /** + * Am I locked? reflects invocation of lock()/unlock() operations locked => !alive (but not in + * the other direction necessarily) locked => !offer, !run, !start, !stop (but this last one is + * obvious since locked => !alive). + */ + protected volatile boolean locked = false; + + /** + * All my subscribers for new message notifications. + */ + @Getter(AccessLevel.NONE) + protected final ArrayList topicListeners = new ArrayList<>(); + + /** + * Instantiates a new Topic Base. + * + * @param servers list of servers + * @param topic topic name + * + * @throws IllegalArgumentException if invalid parameters are present + */ + protected TopicBase(List servers, String topic) { + this(servers, topic, topic); + } + + /** + * Instantiates a new Topic Base. + * + * @param servers list of servers + * @param topic topic name + * + * @throws IllegalArgumentException if invalid parameters are present + */ + protected TopicBase(List servers, String topic, String effectiveTopic) { + + if (servers == null || servers.isEmpty()) { + throw new IllegalArgumentException("Server(s) must be provided"); + } + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException("A Topic must be provided"); + } + + String effectiveTopicCopy; + if (effectiveTopic == null || effectiveTopic.isEmpty()) { + effectiveTopicCopy = topic; + } else { + effectiveTopicCopy = effectiveTopic; + } + + this.servers = servers; + this.topic = topic.toLowerCase(); + this.effectiveTopic = effectiveTopicCopy.toLowerCase(); + } + + @Override + public void register(TopicListener topicListener) { + + logger.info("{}: registering {}", this, topicListener); + + synchronized (this) { + if (topicListener == null) { + throw new IllegalArgumentException("TopicListener must be provided"); + } + + for (TopicListener listener : this.topicListeners) { + if (listener == topicListener) { + return; + } + } + + this.topicListeners.add(topicListener); + } + } + + @Override + public void unregister(TopicListener topicListener) { + + logger.info("{}: unregistering {}", this, topicListener); + + synchronized (this) { + if (topicListener == null) { + throw new IllegalArgumentException("TopicListener must be provided"); + } + + this.topicListeners.remove(topicListener); + } + } + + /** + * Broadcast event to all listeners. + * + * @param message the event + * @return true if all notifications are performed with no error, false otherwise + */ + protected boolean broadcast(String message) { + List snapshotListeners = this.snapshotTopicListeners(); + + var success = true; + for (TopicListener topicListener : snapshotListeners) { + try { + topicListener.onTopicEvent(this.getTopicCommInfrastructure(), this.topic, message); + } catch (Exception e) { + logger.warn("{}: notification error @ {} because of {}", this, topicListener, e.getMessage(), e); + success = false; + } + } + return success; + } + + /** + * Take a snapshot of current topic listeners. + * + * @return the topic listeners + */ + protected synchronized List snapshotTopicListeners() { + @SuppressWarnings("unchecked") + List listeners = (List) topicListeners.clone(); + return listeners; + } + + @Override + public boolean lock() { + + logger.info("{}: locking", this); + + synchronized (this) { + if (this.locked) { + return true; + } + + this.locked = true; + } + + return this.stop(); + } + + @Override + public boolean unlock() { + logger.info("{}: unlocking", this); + + synchronized (this) { + if (!this.locked) { + return true; + } + + this.locked = false; + } + + try { + return this.start(); + } catch (Exception e) { + logger.warn("{}: cannot after unlocking because of {}", this, e.getMessage(), e); + return false; + } + } + + @Override + public synchronized String[] getRecentEvents() { + var events = new String[recentEvents.size()]; + return recentEvents.toArray(events); + } + + + @Override + public String toString() { + return "TopicBase [servers=" + servers + + ", topic=" + topic + + ", effectiveTopic=" + effectiveTopic + + ", #recentEvents=" + recentEvents.size() + + ", locked=" + locked + + ", #topicListeners=" + topicListeners.size() + + "]"; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java new file mode 100644 index 000000000..d98de653b --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java @@ -0,0 +1,87 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Topic Base Factory. + * + * @param Type. + */ +public interface TopicBaseFactory { + + /** + * build a TopicBase instance. + * + * @param properties properties. + * @return T instance. + */ + List build(Properties properties); + + /** + * build a TopicBase instance. + * + * @param servers servers. + * @param topic topic. + * @param managed managed. + * @return T instance. + */ + T build(List servers, String topic, boolean managed); + + /** + * Construct an instance of an endpoint. + * + * @param param parameters + * @return an instance of T. + */ + T build(BusTopicParams param); + + /** + * destroy TopicBase instance. + * @param topic topic. + */ + void destroy(String topic); + + /** + * destroy. + */ + void destroy(); + + /** + * get T instance. + * + * @param topic topic. + * @return T instance. + */ + T get(String topic); + + /** + * inventory of T instances. + * + * @return T instance list. + */ + List inventory(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java new file mode 100644 index 000000000..70ff04e4e --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java @@ -0,0 +1,207 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Topic Factory implementation that indexes T instances in a hash table. + */ +public abstract class TopicBaseHashedFactory implements TopicBaseFactory { + + protected static final String MISSING_TOPIC_MESSAGE = "A topic must be provided"; + protected static final String MISSING_SERVERS_MESSAGE = "Servers must be provided"; + + /** + * endpoints. + */ + protected final HashMap endpoints = new HashMap<>(); + + /** + * get the topic names. + * + * @param properties properties. + * @return list of topic names. + */ + protected abstract List getTopicNames(Properties properties); + + /** + * get the servers that this topic uses. + * + * @param topicName name. + * @param properties properties. + * @return list of servers. + */ + protected abstract List getServers(String topicName, Properties properties); + + /** + * Determines if this topic is managed. + * + * @param topicName name. + * @param properties properties. + * @return if managed. + */ + protected abstract boolean isManaged(String topicName, Properties properties); + + /** + * construct an instance of an endpoint. + * + * @param servers servers, + * @param topic topic. + * @return an instance of T. + */ + public abstract T build(List servers, String topic); + + /** + * {@inheritDoc}. + */ + @Override + public List build(Properties properties) { + List topicNames = getTopicNames(properties); + if (topicNames == null || topicNames.isEmpty()) { + return Collections.emptyList(); + } + + List newEndpoints = new ArrayList<>(); + synchronized (this) { + for (String name : topicNames) { + if (this.endpoints.containsKey(name)) { + newEndpoints.add(this.endpoints.get(name)); + continue; + } + + newEndpoints.add(this.build(getServers(name, properties), name, isManaged(name, properties))); + } + } + return newEndpoints; + } + + /** + * {@inheritDoc}. + */ + @Override + public T build(BusTopicParams param) { + return this.build(param.getServers(), param.getTopic(), param.isManaged()); + } + + /** + * {@inheritDoc}. + */ + @Override + public T build(List servers, String topic, boolean managed) { + if (servers == null || servers.isEmpty()) { + throw new IllegalArgumentException(MISSING_SERVERS_MESSAGE); + } + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC_MESSAGE); + } + + synchronized (this) { + if (this.endpoints.containsKey(topic)) { + return this.endpoints.get(topic); + } + + var endpoint = build(servers, topic); + if (managed) { + this.endpoints.put(topic, endpoint); + } + + return endpoint; + } + } + + /** + * {@inheritDoc}. + */ + @Override + public void destroy(String topic) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC_MESSAGE); + } + + T endpoint; + synchronized (this) { + if (!this.endpoints.containsKey(topic)) { + return; + } + + endpoint = this.endpoints.remove(topic); + } + endpoint.shutdown(); + } + + /** + * {@inheritDoc}. + */ + @Override + public void destroy() { + final List snapshotEndpoints = this.inventory(); + for (final T snapshot : snapshotEndpoints) { + snapshot.shutdown(); + } + + synchronized (this) { + this.endpoints.clear(); + } + } + + /** + * {@inheritDoc}. + */ + @Override + public T get(String topic) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC_MESSAGE); + } + + synchronized (this) { + if (this.endpoints.containsKey(topic)) { + return this.endpoints.get(topic); + } else { + throw new IllegalStateException(topic + " not found"); + } + } + } + + /** + * {@inheritDoc}. + */ + @Override + public List inventory() { + return new ArrayList<>(this.endpoints.values()); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "TopicBaseHashedFactory[ " + super.toString() + " ]"; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java new file mode 100644 index 000000000..131bf2d7f --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java @@ -0,0 +1,114 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import java.util.List; +import lombok.Getter; +import lombok.NonNull; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.event.TopicSink; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client for sending messages to a Topic using TopicSink. + */ +@Getter +public class TopicSinkClient { + private static final Logger logger = LoggerFactory.getLogger(TopicSinkClient.class); + + /** + * Coder used to encode messages being sent to the topic. + */ + private static final Coder CODER = new StandardCoder(); + + /** + * Where messages are published. + */ + private final TopicSink sink; + + /** + * Constructs the object. + * + * @param topic topic to which messages should be published + * @throws TopicSinkClientException if the topic does not exist + */ + public TopicSinkClient(final String topic) throws TopicSinkClientException { + final List lst = getTopicSinks(topic.toLowerCase()); + if (lst.isEmpty()) { + throw new TopicSinkClientException("no sinks for topic: " + topic.toLowerCase()); + } + + this.sink = lst.get(0); + } + + /** + * Constructs the client from a sink object. + * + * @param sink topic sink publisher + */ + public TopicSinkClient(@NonNull TopicSink sink) { + this.sink = sink; + } + + + /** + * Gets the canonical topic name. + * + * @return topic name + */ + public String getTopic() { + return this.sink.getTopic(); + } + + /** + * Sends a message to the topic, after encoding the message as json. + * + * @param message message to be encoded and sent + * @return {@code true} if the message was successfully sent/enqueued, {@code false} otherwise + */ + public boolean send(final Object message) { + try { + final String json = CODER.encode(message); + return sink.send(json); + + } catch (RuntimeException | CoderException e) { + logger.warn("send to {} failed because of {}", sink.getTopic(), e.getMessage(), e); + return false; + } + } + + // the remaining methods are wrappers that can be overridden by junit tests + + /** + * Gets the sinks for a given topic. + * + * @param topic the topic of interest + * @return the sinks for the topic + */ + protected List getTopicSinks(final String topic) { + return TopicEndpointManager.getManager().getTopicSinks(topic); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java new file mode 100644 index 000000000..fad5e1199 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import java.io.Serial; + +/** + * Exception thrown by TopicSink client classes. + */ +public class TopicSinkClientException extends Exception { + @Serial + private static final long serialVersionUID = 1L; + + public TopicSinkClientException() { + super(); + } + + public TopicSinkClientException(final String message) { + super(message); + } + + public TopicSinkClientException(final Throwable cause) { + super(cause); + } + + public TopicSinkClientException(final String message, final Throwable cause) { + super(message, cause); + } + + public TopicSinkClientException(final String message, final Throwable cause, final boolean enableSuppression, + final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java new file mode 100644 index 000000000..0497f1f50 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java @@ -0,0 +1,201 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SINK_TOPICS; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX; + +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.message.bus.utils.KafkaPropertyUtils; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.properties.PropertyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory of KAFKA Reader Topics indexed by topic name. + */ +class IndexedKafkaTopicSinkFactory implements KafkaTopicSinkFactory { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + private static final String MISSING_TOPIC = "A topic must be provided"; + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(IndexedKafkaTopicSinkFactory.class); + + /** + * KAFKA Topic Name Index. + */ + protected HashMap kafkaTopicSinks = new HashMap<>(); + + @Override + public KafkaTopicSink build(BusTopicParams busTopicParams) { + + if (busTopicParams.getServers() == null || busTopicParams.getServers().isEmpty()) { + throw new IllegalArgumentException("KAFKA Server(s) must be provided"); + } + + if (StringUtils.isBlank(busTopicParams.getTopic())) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSinks.containsKey(busTopicParams.getTopic())) { + return kafkaTopicSinks.get(busTopicParams.getTopic()); + } + + KafkaTopicSink kafkaTopicWriter = makeSink(busTopicParams); + if (busTopicParams.isManaged()) { + kafkaTopicSinks.put(busTopicParams.getTopic(), kafkaTopicWriter); + } + + return kafkaTopicWriter; + } + } + + + @Override + public KafkaTopicSink build(List servers, String topic) { + return this.build(BusTopicParams.builder() + .servers(servers) + .topic(topic) + .managed(true) + .useHttps(false) + .build()); + } + + + @Override + public List build(Properties properties) { + + String writeTopics = properties.getProperty(PROPERTY_KAFKA_SINK_TOPICS); + if (StringUtils.isBlank(writeTopics)) { + logger.info("{}: no topic for KAFKA Sink", this); + return new ArrayList<>(); + } + + List newKafkaTopicSinks = new ArrayList<>(); + synchronized (this) { + for (String topic : COMMA_SPACE_PAT.split(writeTopics)) { + addTopic(newKafkaTopicSinks, topic.toLowerCase(), properties); + } + return newKafkaTopicSinks; + } + } + + private void addTopic(List newKafkaTopicSinks, String topic, Properties properties) { + if (this.kafkaTopicSinks.containsKey(topic)) { + newKafkaTopicSinks.add(this.kafkaTopicSinks.get(topic)); + return; + } + + String topicPrefix = PROPERTY_KAFKA_SINK_TOPICS + "." + topic; + + var props = new PropertyUtils(properties, topicPrefix, + (name, value, ex) -> logger.warn("{}: {} {} is in invalid format for topic sink {} ", + this, name, value, topic)); + + String servers = properties.getProperty(topicPrefix + PROPERTY_TOPIC_SERVERS_SUFFIX); + if (StringUtils.isBlank(servers)) { + logger.error("{}: no KAFKA servers configured for sink {}", this, topic); + return; + } + + KafkaTopicSink kafkaTopicWriter = this.build(KafkaPropertyUtils.makeBuilder(props, topic, servers) + .partitionId(props.getString(PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX, null)) + .build()); + newKafkaTopicSinks.add(kafkaTopicWriter); + } + + @Override + public void destroy(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + KafkaTopicSink kafkaTopicWriter; + synchronized (this) { + if (!kafkaTopicSinks.containsKey(topic)) { + return; + } + + kafkaTopicWriter = kafkaTopicSinks.remove(topic); + } + + kafkaTopicWriter.shutdown(); + } + + @Override + public void destroy() { + List writers = this.inventory(); + for (KafkaTopicSink writer : writers) { + writer.shutdown(); + } + + synchronized (this) { + this.kafkaTopicSinks.clear(); + } + } + + @Override + public KafkaTopicSink get(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSinks.containsKey(topic)) { + return kafkaTopicSinks.get(topic); + } else { + throw new IllegalStateException("KafkaTopicSink for " + topic + " not found"); + } + } + } + + @Override + public synchronized List inventory() { + return new ArrayList<>(this.kafkaTopicSinks.values()); + } + + /** + * Makes a new sink. + * + * @param busTopicParams parameters to use to configure the sink + * @return a new sink + */ + protected KafkaTopicSink makeSink(BusTopicParams busTopicParams) { + return new InlineKafkaTopicSink(busTopicParams); + } + + + @Override + public String toString() { + return "IndexedKafkaTopicSinkFactory " + kafkaTopicSinks.keySet(); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java new file mode 100644 index 000000000..1aac89ce3 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java @@ -0,0 +1,211 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.DEFAULT_LIMIT_FETCH; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SOURCE_TOPICS; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_CONSUMER_INSTANCE_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX; + +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.message.bus.utils.KafkaPropertyUtils; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.properties.PropertyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory of KAFKA Source Topics indexed by topic name. + */ +class IndexedKafkaTopicSourceFactory implements KafkaTopicSourceFactory { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + private static final String MISSING_TOPIC = "A topic must be provided"; + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(IndexedKafkaTopicSourceFactory.class); + + /** + * KAFKA Topic Name Index. + */ + protected HashMap kafkaTopicSources = new HashMap<>(); + + @Override + public KafkaTopicSource build(BusTopicParams busTopicParams) { + if (busTopicParams.getServers() == null || busTopicParams.getServers().isEmpty()) { + throw new IllegalArgumentException("KAFKA Server(s) must be provided"); + } + + if (busTopicParams.getTopic() == null || busTopicParams.getTopic().isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSources.containsKey(busTopicParams.getTopic())) { + return kafkaTopicSources.get(busTopicParams.getTopic()); + } + + var kafkaTopicSource = makeSource(busTopicParams); + + kafkaTopicSources.put(busTopicParams.getTopic(), kafkaTopicSource); + + return kafkaTopicSource; + } + } + + @Override + public List build(Properties properties) { + + String readTopics = properties.getProperty(PROPERTY_KAFKA_SOURCE_TOPICS); + if (StringUtils.isBlank(readTopics)) { + logger.info("{}: no topic for KAFKA Source", this); + return new ArrayList<>(); + } + + List newKafkaTopicSources = new ArrayList<>(); + synchronized (this) { + for (String topic : COMMA_SPACE_PAT.split(readTopics)) { + addTopic(newKafkaTopicSources, topic.toLowerCase(), properties); + } + } + return newKafkaTopicSources; + } + + @Override + public KafkaTopicSource build(List servers, String topic) { + return this.build(BusTopicParams.builder() + .servers(servers) + .topic(topic) + .managed(true) + .fetchTimeout(DEFAULT_TIMEOUT_MS_FETCH) + .fetchLimit(DEFAULT_LIMIT_FETCH) + .useHttps(false).build()); + } + + private void addTopic(List newKafkaTopicSources, String topic, Properties properties) { + if (this.kafkaTopicSources.containsKey(topic)) { + newKafkaTopicSources.add(this.kafkaTopicSources.get(topic)); + return; + } + + String topicPrefix = PROPERTY_KAFKA_SOURCE_TOPICS + "." + topic; + + var props = new PropertyUtils(properties, topicPrefix, + (name, value, ex) -> logger.warn("{}: {} {} is in invalid format for topic source {} ", + this, name, value, topic)); + + String servers = properties.getProperty(topicPrefix + PROPERTY_TOPIC_SERVERS_SUFFIX); + if (StringUtils.isBlank(servers)) { + logger.error("{}: no KAFKA servers configured for source {}", this, topic); + return; + } + + var kafkaTopicSource = this.build(KafkaPropertyUtils.makeBuilder(props, topic, servers) + .consumerGroup(props.getString( + PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX, null)) + .consumerInstance(props.getString( + PROPERTY_TOPIC_SOURCE_CONSUMER_INSTANCE_SUFFIX, null)) + .fetchTimeout(props.getInteger( + PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX, + DEFAULT_TIMEOUT_MS_FETCH)) + .fetchLimit(props.getInteger(PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX, + DEFAULT_LIMIT_FETCH)) + .build()); + + newKafkaTopicSources.add(kafkaTopicSource); + } + + /** + * Makes a new source. + * + * @param busTopicParams parameters to use to configure the source + * @return a new source + */ + protected KafkaTopicSource makeSource(BusTopicParams busTopicParams) { + return new SingleThreadedKafkaTopicSource(busTopicParams); + } + + @Override + public void destroy(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + KafkaTopicSource kafkaTopicSource; + + synchronized (this) { + if (!kafkaTopicSources.containsKey(topic)) { + return; + } + + kafkaTopicSource = kafkaTopicSources.remove(topic); + } + + kafkaTopicSource.shutdown(); + } + + @Override + public void destroy() { + List readers = this.inventory(); + for (KafkaTopicSource reader : readers) { + reader.shutdown(); + } + + synchronized (this) { + this.kafkaTopicSources.clear(); + } + } + + @Override + public KafkaTopicSource get(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSources.containsKey(topic)) { + return kafkaTopicSources.get(topic); + } else { + throw new IllegalStateException("KafkaTopiceSource for " + topic + " not found"); + } + } + } + + @Override + public synchronized List inventory() { + return new ArrayList<>(this.kafkaTopicSources.values()); + } + + @Override + public String toString() { + return "IndexedKafkaTopicSourceFactory " + kafkaTopicSources.keySet(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java new file mode 100644 index 000000000..4bdd2b0f1 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.Map; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.base.InlineBusTopicSink; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation publishes events for the associated KAFKA topic, inline with the calling + * thread. + */ +public class InlineKafkaTopicSink extends InlineBusTopicSink implements KafkaTopicSink { + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(InlineKafkaTopicSink.class); + + protected Map additionalProps; + + /** + * Argument-based KAFKA Topic Writer instantiation. BusTopicParams contains the below + * attributes. + * + *

servers list of KAFKA servers available for publishing + * topic the topic to publish to + * partitionId the partition key (optional, autogenerated if not provided) + * useHttps does connection use HTTPS? + * @param busTopicParams contains attributes needed + * @throws IllegalArgumentException if invalid arguments are detected + */ + public InlineKafkaTopicSink(BusTopicParams busTopicParams) { + super(busTopicParams); + this.additionalProps = busTopicParams.getAdditionalProps(); + } + + /** + * Instantiation of internal resources. + */ + @Override + public void init() { + + this.publisher = new KafkaPublisherWrapper(BusTopicParams.builder() + .servers(this.servers) + .topic(this.effectiveTopic) + .useHttps(this.useHttps) + .allowTracing(this.allowTracing) + .additionalProps(this.additionalProps) + .build()); + logger.info("{}: KAFKA SINK created", this); + } + + @Override + public String toString() { + return "InlineKafkaTopicSink [getTopicCommInfrastructure()=" + getTopicCommInfrastructure() + ", toString()=" + + super.toString() + "]"; + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return Topic.CommInfrastructure.KAFKA; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java new file mode 100644 index 000000000..86b9e9366 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingProducerInterceptor; +import java.util.Properties; +import java.util.UUID; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.onap.policy.common.message.bus.event.base.BusPublisher; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Kafka based library publisher. + */ +public class KafkaPublisherWrapper implements BusPublisher { + + private static final Logger logger = LoggerFactory.getLogger(KafkaPublisherWrapper.class); + private static final String KEY_SERIALIZER = "org.apache.kafka.common.serialization.StringSerializer"; + + private final String topic; + + /** + * Kafka publisher. + */ + private final Producer producer; + protected Properties kafkaProps; + + /** + * Kafka Publisher Wrapper. + * + * @param busTopicParams topic parameters + */ + public KafkaPublisherWrapper(BusTopicParams busTopicParams) { + + if (busTopicParams.isTopicInvalid()) { + throw new IllegalArgumentException("No topic for Kafka"); + } + + this.topic = busTopicParams.getTopic(); + + // Setup Properties for consumer + kafkaProps = new Properties(); + kafkaProps.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, busTopicParams.getServers().get(0)); + if (busTopicParams.isAdditionalPropsValid()) { + kafkaProps.putAll(busTopicParams.getAdditionalProps()); + } + + if (kafkaProps.get(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, KEY_SERIALIZER); + } + + if (kafkaProps.get(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KEY_SERIALIZER); + } + + if (busTopicParams.isAllowTracing()) { + kafkaProps.setProperty(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, + TracingProducerInterceptor.class.getName()); + } + + producer = new KafkaProducer<>(kafkaProps); + } + + @Override + public boolean send(String partitionId, String message) { + if (message == null) { + throw new IllegalArgumentException(NO_MESSAGE_PROVIDED); + } + + try { + // Create the record + ProducerRecord producerRecord = + new ProducerRecord<>(topic, UUID.randomUUID().toString(), message); + + this.producer.send(producerRecord); + producer.flush(); + } catch (Exception e) { + logger.warn("{}: SEND of {} cannot be performed because of {}", this, message, e.getMessage(), e); + return false; + } + return true; + } + + @Override + public void close() { + logger.info(LOG_CLOSE, this); + + try { + this.producer.close(); + } catch (Exception e) { + logger.warn("{}: CLOSE FAILED because of {}", this, e.getMessage(), e); + } + } + + @Override + public String toString() { + return "KafkaPublisherWrapper []"; + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java new file mode 100644 index 000000000..c10285fcc --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class KafkaTopicFactories { + + /** + * Factory for instantiation and management of sinks. + */ + @Getter + private static final KafkaTopicSinkFactory sinkFactory = new IndexedKafkaTopicSinkFactory(); + + /** + * Factory for instantiation and management of sources. + */ + @Getter + private static final KafkaTopicSourceFactory sourceFactory = new IndexedKafkaTopicSourceFactory(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java new file mode 100644 index 000000000..f784a9876 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java @@ -0,0 +1,28 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import org.onap.policy.common.message.bus.event.base.BusTopicSink; + +/** + * Topic Writer over KAFKA Infrastructure. + */ +public interface KafkaTopicSink extends BusTopicSink { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java new file mode 100644 index 000000000..8feecbe05 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java @@ -0,0 +1,89 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * KAFKA Topic Sink Factory. + */ +public interface KafkaTopicSinkFactory { + + /** + * Instantiates a new KAFKA Topic Writer. + * + * @param busTopicParams parameters object + * @return an KAFKA Topic Sink + */ + KafkaTopicSink build(BusTopicParams busTopicParams); + + /** + * Creates an KAFKA Topic Writer based on properties files. + * + * @param properties Properties containing initialization values + * + * @return an KAFKA Topic Writer + * @throws IllegalArgumentException if invalid parameters are present + */ + List build(Properties properties); + + /** + * Instantiates a new KAFKA Topic Writer. + * + * @param servers list of servers + * @param topic topic name + * + * @return an KAFKA Topic Writer + * @throws IllegalArgumentException if invalid parameters are present + */ + KafkaTopicSink build(List servers, String topic); + + /** + * Destroys an KAFKA Topic Writer based on a topic. + * + * @param topic topic name + * @throws IllegalArgumentException if invalid parameters are present + */ + void destroy(String topic); + + /** + * Destroys all KAFKA Topic Writers. + */ + void destroy(); + + /** + * gets an KAFKA Topic Writer based on topic name. + * + * @param topic the topic name + * + * @return an KAFKA Topic Writer with topic name + * @throws IllegalArgumentException if an invalid topic is provided + * @throws IllegalStateException if the KAFKA Topic Reader is an incorrect state + */ + KafkaTopicSink get(String topic); + + /** + * Provides a snapshot of the KAFKA Topic Writers. + * + * @return a list of the KAFKA Topic Writers + */ + List inventory(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java new file mode 100644 index 000000000..bddced7a1 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java @@ -0,0 +1,28 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import org.onap.policy.common.message.bus.event.base.BusTopicSource; + +/** + * Kafka Topic Source. + */ +public interface KafkaTopicSource extends BusTopicSource { + +} \ No newline at end of file diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java new file mode 100644 index 000000000..06f4412c9 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java @@ -0,0 +1,88 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Kafka Topic Source Factory. + */ +public interface KafkaTopicSourceFactory { + + /** + * Creates a Kafka Topic Source based on properties files. + * + * @param properties Properties containing initialization values + * + * @return a Kafka Topic Source + * @throws IllegalArgumentException if invalid parameters are present + */ + List build(Properties properties); + + /** + * Instantiates a new Kafka Topic Source. + * + * @param busTopicParams parameters object + * @return a Kafka Topic Source + */ + KafkaTopicSource build(BusTopicParams busTopicParams); + + /** + * Instantiates a new Kafka Topic Source. + * + * @param servers list of servers + * @param topic topic name + * + * @return a Kafka Topic Source + * @throws IllegalArgumentException if invalid parameters are present + */ + KafkaTopicSource build(List servers, String topic); + + /** + * Destroys a Kafka Topic Source based on a topic. + * + * @param topic topic name + * @throws IllegalArgumentException if invalid parameters are present + */ + void destroy(String topic); + + /** + * Destroys all Kafka Topic Sources. + */ + void destroy(); + + /** + * Gets a Kafka Topic Source based on topic name. + * + * @param topic the topic name + * @return a Kafka Topic Source with topic name + * @throws IllegalArgumentException if an invalid topic is provided + * @throws IllegalStateException if the Kafka Topic Source is an incorrect state + */ + KafkaTopicSource get(String topic); + + /** + * Provides a snapshot of the Kafka Topic Sources. + * + * @return a list of the Kafka Topic Sources + */ + List inventory(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java new file mode 100644 index 000000000..5691cb127 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.Map; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.base.BusConsumer; +import org.onap.policy.common.message.bus.event.base.SingleThreadedBusTopicSource; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * This topic source implementation specializes in reading messages over a Kafka Bus topic source and + * notifying its listeners. + */ +public class SingleThreadedKafkaTopicSource extends SingleThreadedBusTopicSource implements KafkaTopicSource { + + protected Map additionalProps = null; + + /** + * Constructor. + * + * @param busTopicParams Parameters object containing all the required inputs + * @throws IllegalArgumentException An invalid parameter passed in + */ + public SingleThreadedKafkaTopicSource(BusTopicParams busTopicParams) { + super(busTopicParams); + this.additionalProps = busTopicParams.getAdditionalProps(); + try { + this.init(); + } catch (Exception e) { + throw new IllegalArgumentException("ERROR during init in kafka-source: cannot create topic " + topic, e); + } + } + + /** + * Initialize the client. + */ + @Override + public void init() { + BusTopicParams.TopicParamsBuilder builder = BusTopicParams.builder() + .servers(this.servers) + .topic(this.effectiveTopic) + .fetchTimeout(this.fetchTimeout) + .consumerGroup(this.consumerGroup) + .useHttps(this.useHttps) + .allowTracing(this.allowTracing); + + this.consumer = new BusConsumer.KafkaConsumerWrapper(builder + .additionalProps(this.additionalProps) + .build()); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return Topic.CommInfrastructure.KAFKA; + } + + @Override + public String toString() { + return "SingleThreadedKafkaTopicSource [getTopicCommInfrastructure()=" + getTopicCommInfrastructure() + + ", toString()=" + super.toString() + "]"; + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java new file mode 100644 index 000000000..e25aca54c --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java @@ -0,0 +1,141 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import java.util.List; +import org.onap.policy.common.message.bus.event.base.TopicBase; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * No Operation topic endpoint. + */ +public abstract class NoopTopicEndpoint extends TopicBase { + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(NoopTopicEndpoint.class); + + /** + * Constructs the object. + */ + protected NoopTopicEndpoint(List servers, String topic) { + super(servers, topic); + } + + /** + * I/O. + * + * @param type "IN" or "OUT". + * @param message message. + * @return true if successful. + */ + protected boolean io(EventType type, String message) { + + if (message == null || message.isEmpty()) { + throw new IllegalArgumentException("Message is empty"); + } + + if (!this.alive) { + throw new IllegalStateException(this + " is stopped"); + } + + try { + synchronized (this) { + this.recentEvents.add(message); + } + + NetLoggerUtil.log(type, this.getTopicCommInfrastructure(), this.topic, message); + + broadcast(message); + } catch (Exception e) { + logger.warn("{}: cannot send because of {}", this, e.getMessage(), e); + return false; + } + + return true; + } + + /** + * {@inheritDoc}. + */ + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean start() { + logger.info("{}: starting", this); + + synchronized (this) { + if (!this.alive) { + if (locked) { + throw new IllegalStateException(this + " is locked."); + } + + this.alive = true; + } + } + + return true; + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean stop() { + logger.info("{}: stopping", this); + + synchronized (this) { + this.alive = false; + } + return true; + } + + /** + * {@inheritDoc}. + */ + @Override + public void shutdown() { + logger.info("{}: shutdown", this); + + this.stop(); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicEndpoint[" + super.toString() + "]"; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java new file mode 100644 index 000000000..506cc9f9f --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java @@ -0,0 +1,42 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NoopTopicFactories { + + /** + * Factory for instantiation and management of sinks. + */ + @Getter + private static final NoopTopicSinkFactory sinkFactory = new NoopTopicSinkFactory(); + + /** + * Factory for instantiation and management of sources. + */ + @Getter + private static final NoopTopicSourceFactory sourceFactory = new NoopTopicSourceFactory(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java new file mode 100644 index 000000000..e81dfaea3 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java @@ -0,0 +1,117 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicBaseHashedFactory; + +/** + * Noop Topic Factory. + */ +public abstract class NoopTopicFactory extends TopicBaseHashedFactory { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + + /** + * Get Topics Property Name. + * + * @return property name. + */ + protected abstract String getTopicsPropertyName(); + + /** + * {@inheritDoc}. + */ + @Override + protected List getTopicNames(Properties properties) { + String topics = properties.getProperty(getTopicsPropertyName()); + if (topics == null || topics.isEmpty()) { + return new ArrayList<>(); + } + + return Arrays.asList(COMMA_SPACE_PAT.split(topics)); + } + + /** + * {@inheritDoc}. + */ + @Override + protected List getServers(String topicName, Properties properties) { + String servers = + properties.getProperty(getTopicsPropertyName() + "." + topicName + + PROPERTY_TOPIC_SERVERS_SUFFIX); + + if (servers == null || servers.isEmpty()) { + servers = CommInfrastructure.NOOP.toString(); + } + + return new ArrayList<>(Arrays.asList(COMMA_SPACE_PAT.split(servers))); + } + + /** + * {@inheritDoc}. + */ + @Override + protected boolean isManaged(String topicName, Properties properties) { + var managedString = + properties.getProperty(getTopicsPropertyName() + "." + topicName + PROPERTY_MANAGED_SUFFIX); + + var managed = true; + if (managedString != null && !managedString.isEmpty()) { + managed = Boolean.parseBoolean(managedString); + } + + return managed; + } + + /** + * {@inheritDoc}. + */ + @Override + public T build(List serverList, String topic, boolean managed) { + List servers; + if (serverList == null || serverList.isEmpty()) { + servers = Collections.singletonList(CommInfrastructure.NOOP.toString()); + } else { + servers = serverList; + } + + return super.build(servers, topic, managed); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicFactory[ " + super.toString() + " ]"; + } +} + diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java new file mode 100644 index 000000000..b7d78ff46 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import java.util.List; +import org.onap.policy.common.message.bus.event.TopicSink; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; + +/** + * No Operation Topic Sink. + */ +public class NoopTopicSink extends NoopTopicEndpoint implements TopicSink { + + /** + * Constructs the object. + */ + public NoopTopicSink(List servers, String topic) { + super(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean send(String message) { + return super.io(EventType.OUT, message); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSink[" + super.toString() + "]"; + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java new file mode 100644 index 000000000..cdf4a17b3 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_NOOP_SINK_TOPICS; + +import java.util.List; + +/** + * Noop Topic Sink Factory. + */ +public class NoopTopicSinkFactory extends NoopTopicFactory { + + /** + * {@inheritDoc}. + */ + @Override + protected String getTopicsPropertyName() { + return PROPERTY_NOOP_SINK_TOPICS; + } + + /** + * {@inheritDoc}. + */ + @Override + public NoopTopicSink build(List servers, String topic) { + return new NoopTopicSink(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSinkFactory [" + super.toString() + "]"; + } + +} + diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java new file mode 100644 index 000000000..6e7d02161 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import java.util.List; +import org.onap.policy.common.message.bus.event.TopicSource; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; + +/** + * No Operation Topic Source. + */ +public class NoopTopicSource extends NoopTopicEndpoint implements TopicSource { + + /** + * Constructs the object. + */ + public NoopTopicSource(List servers, String topic) { + super(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean offer(String event) { + return super.io(EventType.IN, event); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSource[" + super.toString() + "]"; + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java new file mode 100644 index 000000000..5e3a365a3 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_NOOP_SOURCE_TOPICS; + +import java.util.List; + +/** + * No Operation Topic Source Factory. + */ +public class NoopTopicSourceFactory extends NoopTopicFactory { + + /** + * {@inheritDoc}. + */ + @Override + protected String getTopicsPropertyName() { + return PROPERTY_NOOP_SOURCE_TOPICS; + } + + /** + * {@inheritDoc}. + */ + @Override + public NoopTopicSource build(List servers, String topic) { + return new NoopTopicSource(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSourceFactory [" + super.toString() + "]"; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java new file mode 100644 index 000000000..6e3b8301e --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.features; + +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.services.OrderedService; +import org.slf4j.Logger; + +/** + * Logging Feature API. Provides interception points before and after logging a message. + */ +public interface NetLoggerFeatureApi extends OrderedService { + + /** + * Intercepts a message before it is logged. + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise. + */ + default boolean beforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return false; + } + + /** + * Intercepts a message after it is logged. + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise. + */ + default boolean afterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return false; + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java new file mode 100644 index 000000000..4f57ab2af --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.features; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.onap.policy.common.utils.services.OrderedServiceImpl; + +/** + * Providers for network logging feature. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NetLoggerFeatureProviders { + + /** + * Feature providers implementing this interface. + */ + @Getter + private static final OrderedServiceImpl providers = + new OrderedServiceImpl<>(NetLoggerFeatureApi.class); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheck.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheck.java new file mode 100644 index 000000000..f371eda9b --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheck.java @@ -0,0 +1,25 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck; + +import java.util.List; + +public interface TopicHealthCheck { + boolean healthCheck(List topics); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactory.java new file mode 100644 index 000000000..d77905048 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactory.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck; + +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.healthcheck.kafka.KafkaHealthCheck; +import org.onap.policy.common.message.bus.healthcheck.noop.NoopHealthCheck; +import org.onap.policy.common.parameters.topic.TopicParameters; + +public class TopicHealthCheckFactory { + + /** + * Get Topic HealthCheck. + * + * @param param TopicParameters + * @return TopicHealthCheck + */ + public TopicHealthCheck getTopicHealthCheck(TopicParameters param) { + return switch (Topic.CommInfrastructure.valueOf(param.getTopicCommInfrastructure().toUpperCase())) { + case KAFKA -> new KafkaHealthCheck(param); + case NOOP -> new NoopHealthCheck(); + default -> null; + }; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheck.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheck.java new file mode 100644 index 000000000..ef8a0f76c --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheck.java @@ -0,0 +1,109 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck.kafka; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.common.KafkaException; +import org.onap.policy.common.message.bus.healthcheck.TopicHealthCheck; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class KafkaHealthCheck implements TopicHealthCheck { + + private static final Logger logger = LoggerFactory.getLogger(KafkaHealthCheck.class); + private final TopicParameters parameters; + + public KafkaHealthCheck(TopicParameters parameters) { + this.parameters = parameters; + } + + /** + * Check that Kafka is OnLine and topics are available. + * + * @return true if Kafka is OnLine + */ + public boolean healthCheck(List topics) { + if (parameters.getServers() == null || parameters.getServers().isEmpty()) { + logger.warn("Kafka Address not defined!"); + return true; + } + try (var client = createAdminClient()) { + if (!checkConnection(client)) { + logger.warn("Kafka not UP yet!"); + return false; + } + if (topics.isEmpty()) { + logger.warn("Kafka is UP"); + return true; + } + + return checkTopics(client, topics); + } catch (KafkaException | ExecutionException e) { + logger.error(e.getMessage()); + return false; + } catch (InterruptedException e) { + logger.error(e.getMessage()); + Thread.currentThread().interrupt(); + return false; + } + } + + private boolean checkConnection(AdminClient client) throws ExecutionException, InterruptedException { + var nodes = client.describeCluster().nodes().get(); + if (nodes == null || nodes.isEmpty()) { + return false; + } + nodes.forEach(node -> logger.debug("nodeId {}", node.id())); + return true; + } + + private boolean checkTopics(AdminClient client, List topics) + throws ExecutionException, InterruptedException { + var listTopics = client.listTopics().names().get(); + if (listTopics == null || listTopics.isEmpty()) { + logger.warn("Kafka topics not available!"); + return false; + } + var setTopics = listTopics.stream().map(String::toLowerCase).collect(Collectors.toSet()); + for (var topic : topics) { + if (!setTopics.contains(topic.toLowerCase())) { + logger.warn("Kafka topic {} not available!", topic); + return false; + } + } + logger.info("Kafka is UP and topics available!"); + return true; + } + + protected AdminClient createAdminClient() { + var kafkaProps = new Properties(); + kafkaProps.setProperty(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, parameters.getServers().get(0)); + + if (parameters.isAdditionalPropsValid()) { + kafkaProps.putAll(parameters.getAdditionalProps()); + } + return AdminClient.create(kafkaProps); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheck.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheck.java new file mode 100644 index 000000000..c0006abf4 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheck.java @@ -0,0 +1,67 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck.noop; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.TopicEndpoint; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.healthcheck.TopicHealthCheck; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NoopHealthCheck implements TopicHealthCheck { + + private final Logger logger = LoggerFactory.getLogger(NoopHealthCheck.class); + + private final TopicEndpoint topicEndpoint = TopicEndpointManager.getManager(); + + private Map actualTopics; + + @Override + public boolean healthCheck(List topics) { + var topicsHealthy = true; + + this.populateActualTopics(); + + if (topicEndpoint.isAlive()) { + for (String topic : topics) { + var actualTopic = actualTopics.get(topic.toLowerCase()); + if (!actualTopic.isAlive()) { + logger.warn("Topic {} is not alive!", topic); + topicsHealthy = false; + break; + } + } + } else { + logger.warn("Topic Endpoint is not alive!"); + return false; + } + + return topicsHealthy; + } + + private void populateActualTopics() { + actualTopics = new HashMap<>(); + topicEndpoint.getNoopTopicSinks().forEach(sink -> actualTopics.put(sink.getTopic(), sink)); + topicEndpoint.getNoopTopicSources().forEach(source -> actualTopics.put(source.getTopic(), source)); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java new file mode 100644 index 000000000..9aa529f35 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java @@ -0,0 +1,78 @@ +/*- + * ============LICENSE_START=============================================== + * Copyright (C) 2024 Nordix Foundation. + * ======================================================================== + * 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. + * ============LICENSE_END================================================= + */ + +package org.onap.policy.common.message.bus.properties; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class MessageBusProperties { + + /* Generic property suffixes */ + + public static final String PROPERTY_TOPIC_SERVERS_SUFFIX = ".servers"; + public static final String PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX = ".effectiveTopic"; + + public static final String PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX = ".consumerGroup"; + public static final String PROPERTY_TOPIC_SOURCE_CONSUMER_INSTANCE_SUFFIX = ".consumerInstance"; + public static final String PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX = ".fetchTimeout"; + public static final String PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX = ".fetchLimit"; + public static final String PROPERTY_MANAGED_SUFFIX = ".managed"; + public static final String PROPERTY_ADDITIONAL_PROPS_SUFFIX = ".additionalProps"; + + public static final String PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX = ".partitionKey"; + + public static final String PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX = ".selfSignedCertificates"; + + public static final String PROPERTY_NOOP_SOURCE_TOPICS = "noop.source.topics"; + public static final String PROPERTY_NOOP_SINK_TOPICS = "noop.sink.topics"; + + /* KAFKA Properties */ + + public static final String PROPERTY_KAFKA_SOURCE_TOPICS = "kafka.source.topics"; + public static final String PROPERTY_KAFKA_SINK_TOPICS = "kafka.sink.topics"; + + /* HTTP Server Properties */ + + public static final String PROPERTY_HTTP_HTTPS_SUFFIX = ".https"; + + /* Topic Sink Values */ + + /* Topic Source values */ + + /** + * Default Timeout fetching in milliseconds. + */ + public static final int DEFAULT_TIMEOUT_MS_FETCH = 15000; + + /** + * Default maximum number of messages fetch at the time. + */ + public static final int DEFAULT_LIMIT_FETCH = 100; + + /** + * Definition of No Timeout fetching. + */ + public static final int NO_TIMEOUT_MS_FETCH = -1; + + /** + * Definition of No limit fetching. + */ + public static final int NO_LIMIT_FETCH = -1; +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java new file mode 100644 index 000000000..cfe622085 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ADDITIONAL_PROPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.parameters.topic.BusTopicParams.TopicParamsBuilder; +import org.onap.policy.common.utils.properties.PropertyUtils; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class KafkaPropertyUtils { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + + /** + * Makes a topic builder, configuring it with properties that are common to both + * sources and sinks. + * + * @param props properties to be used to configure the builder + * @param topic topic being configured + * @param servers target servers + * @return a topic builder + */ + public static TopicParamsBuilder makeBuilder(PropertyUtils props, String topic, String servers) { + + final List serverList = new ArrayList<>(Arrays.asList(COMMA_SPACE_PAT.split(servers))); + return BusTopicParams.builder() + .servers(serverList) + .topic(topic) + .effectiveTopic(props.getString(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX, topic)) + .managed(props.getBoolean(PROPERTY_MANAGED_SUFFIX, true)) + .additionalProps(getAdditionalProps(props.getString(PROPERTY_ADDITIONAL_PROPS_SUFFIX, ""))); + } + + private static Map getAdditionalProps(String additionalPropsString) { + try { + Map additionalProps = new HashMap<>(); + var converted = new ObjectMapper().readValue(additionalPropsString, Map.class); + converted.forEach((k, v) -> { + if (k instanceof String key && v instanceof String value) { + additionalProps.put(key, value); + } + }); + return additionalProps; + } catch (Exception e) { + return Collections.emptyMap(); + } + + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java new file mode 100644 index 000000000..b5454e5c0 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java @@ -0,0 +1,134 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import lombok.Getter; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.features.NetLoggerFeatureProviders; +import org.onap.policy.common.utils.services.FeatureApiUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A network logging utility class that allows drools applications code to access the + * network log (or other specified loggers) and logging features. + * + */ +public class NetLoggerUtil { + + /** + * Loggers. + */ + private static final Logger logger = LoggerFactory.getLogger(NetLoggerUtil.class); + @Getter + private static final Logger networkLogger = LoggerFactory.getLogger("network"); + + /** + * Constant for the system line separator. + */ + public static final String SYSTEM_LS = System.lineSeparator(); + + /** + * Specifies if the message is coming in or going out. + */ + public enum EventType { + IN, OUT + } + + /** + * Logs a message to the network logger. + * + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is REST + * @param message message to be logged + */ + public static void log(EventType type, CommInfrastructure protocol, String topic, String message) { + log(networkLogger, type, protocol, topic, message); + } + + /** + * Logs a message to the specified logger (i.e. a controller logger). + * + * @param eventLogger the logger that will have the message appended + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is REST + * @param message message to be logged + */ + public static void log(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + if (eventLogger == null) { + logger.debug("the logger is null, defaulting to network logger"); + eventLogger = networkLogger; + } + + if (featureBeforeLog(eventLogger, type, protocol, topic, message)) { + return; + } + + eventLogger.info("[{}|{}|{}]{}{}", type, protocol, topic, SYSTEM_LS, message); + + featureAfterLog(eventLogger, type, protocol, topic, message); + } + + /** + * Executes features that pre-process a message before it is logged. + * + * @param eventLogger the logger that will have the message appended + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is REST + * @param message message to be logged + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise + */ + private static boolean featureBeforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, + String topic, String message) { + + return FeatureApiUtils.apply(NetLoggerFeatureProviders.getProviders().getList(), + feature -> feature.beforeLog(eventLogger, type, protocol, topic, message), + (feature, ex) -> logger.error("feature {} before-log failure because of {}", + feature.getClass().getName(), ex.getMessage(), ex)); + } + + /** + * Executes features that post-process a message after it is logged. + * + * @param eventLogger the logger that will have the message appended + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is rest + * @param message message to be logged + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise + */ + private static boolean featureAfterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, + String topic, String message) { + + return FeatureApiUtils.apply(NetLoggerFeatureProviders.getProviders().getList(), + feature -> feature.afterLog(eventLogger, type, protocol, topic, message), + (feature, ex) -> logger.error("feature {} after-log failure because of {}", + feature.getClass().getName(), ex.getMessage(), ex)); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java b/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java new file mode 100644 index 000000000..e16205308 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java @@ -0,0 +1,210 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * This class holds the result of the validation of an arbitrary bean. + */ +public class BeanValidationResult extends ValidationResultImpl { + + /** + * Validation results for each item in the bean. + */ + private final List itemResults = new ArrayList<>(); + + + /** + * Constructs the object. + * + * @param name name of the bean being validated + * @param object object being validated + */ + public BeanValidationResult(String name, Object object) { + super(name, object); + } + + /** + * Adds a result to this result. + * + * @param result the result to be added + * @return {@code true} if the result is {@code null} or valid, {@code false} if the + * result is invalid + */ + public boolean addResult(ValidationResult result) { + if (result == null) { + return true; + } + + itemResults.add(result); + setResult(result.getStatus()); + + return result.isValid(); + } + + /** + * Adds a result to this result. + * @param name name of the object of this result + * @param object object being validated + * @param status status of the new result + * @param message new result message + * @return {@code true} if the status is {@code null} or valid, {@code false} if the + * status is invalid + */ + public boolean addResult(String name, Object object, ValidationStatus status, String message) { + return addResult(new ObjectValidationResult(name, object, status, message)); + } + + /** + * Validates that a sub-object within the bean is not {@code null}. + * + * @param subName name of the sub-object + * @param subObject the sub-object + * @return {@code true} if the value is not null, {@code false} otherwise + */ + public boolean validateNotNull(String subName, Object subObject) { + var result = new ObjectValidationResult(subName, subObject); + + if (result.validateNotNull()) { + return true; + + } else { + addResult(result); + return false; + } + } + + /** + * Validates the items in a list, after validating that the list, itself, is not null. + * + * @param listName name of the list + * @param list list whose items are to be validated, or {@code null} + * @param itemValidator function to validate an item in the list + * @return {@code true} if all items in the list are valid, {@code false} otherwise + */ + public boolean validateNotNullList(String listName, Collection list, + Function itemValidator) { + + return validateNotNull(listName, list) && validateList(listName, list, itemValidator); + } + + /** + * Validates the items in a list. + * + * @param listName name of the list + * @param list list whose items are to be validated, or {@code null} + * @param itemValidator function to validate an item in the list + * @return {@code true} if all items in the list are valid, {@code false} otherwise + */ + public boolean validateList(String listName, Collection list, Function itemValidator) { + if (list == null) { + return true; + } + + var result = new BeanValidationResult(listName, null); + for (T item : list) { + if (item == null) { + result.addResult("item", item, ValidationStatus.INVALID, "null"); + } else { + result.addResult(itemValidator.apply(item)); + } + } + + if (result.isValid()) { + return true; + + } else { + addResult(result); + return false; + } + } + + /** + * Validates the entries in a map. + * + * @param mapName name of the list + * @param map map whose entries are to be validated, or {@code null} + * @param entryValidator function to validate an entry in the map + * @return {@code true} if all entries in the map are valid, {@code false} otherwise + */ + public boolean validateMap(String mapName, Map map, + BiConsumer> entryValidator) { + if (map == null) { + return true; + } + + var result = new BeanValidationResult(mapName, null); + for (Entry ent : map.entrySet()) { + entryValidator.accept(result, ent); + } + + if (result.isValid()) { + return true; + + } else { + addResult(result); + return false; + } + } + + /** + * Gets the validation result. + * + * @param initialIndentation the indentation to use on the main result output + * @param subIndentation the indentation to use on sub parts of the result output + * @param showClean output information on clean fields + * @return the result + */ + @Override + public String getResult(final String initialIndentation, final String subIndentation, final boolean showClean) { + if (!showClean && getStatus() == ValidationStatus.CLEAN) { + return null; + } + + var builder = new StringBuilder(); + + builder.append(initialIndentation); + builder.append('"'); + builder.append(getName()); + + builder.append("\" "); + builder.append(getStatus()); + builder.append(", "); + builder.append(getMessage()); + builder.append('\n'); + + for (ValidationResult itemResult : itemResults) { + String message = itemResult.getResult(initialIndentation + subIndentation, subIndentation, showClean); + if (message != null) { + builder.append(message); + } + } + + return builder.toString(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidator.java new file mode 100644 index 000000000..c4244b274 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidator.java @@ -0,0 +1,433 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.parameters.annotations.ClassName; +import org.onap.policy.common.parameters.annotations.Max; +import org.onap.policy.common.parameters.annotations.Min; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; +import org.onap.policy.common.parameters.annotations.Pattern; +import org.onap.policy.common.parameters.annotations.Size; +import org.onap.policy.common.parameters.annotations.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bean validator, supporting the parameter annotations. + */ +public class BeanValidator { + public static final Logger logger = LoggerFactory.getLogger(BeanValidator.class); + + /** + * Validates top level fields within an object. For each annotated field, it retrieves + * the value using the public "getter" method for the field. If there is no public + * "getter" method, then it throws an exception. Otherwise, it validates the retrieved + * value based on the annotations. This recurses through super classes looking for + * fields to be verified, but it does not examine any interfaces. + * + * @param name name of the object being validated + * @param object object to be validated. If {@code null}, then an empty result is + * returned + * @return the validation result + */ + public BeanValidationResult validateTop(String name, Object object) { + var result = new BeanValidationResult(name, object); + if (object == null) { + return result; + } + + // check class hierarchy - don't need to check interfaces + for (Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + validateFields(result, object, clazz); + } + + return result; + } + + /** + * Adds validators based on the annotations that are available. + * + * @param validator where to add the validators + */ + protected void addValidators(ValueValidator validator) { + validator.addAnnotation(NotNull.class, this::verNotNull); + validator.addAnnotation(NotBlank.class, this::verNotBlank); + validator.addAnnotation(Size.class, this::verSize); + validator.addAnnotation(Max.class, this::verMax); + validator.addAnnotation(Min.class, this::verMin); + validator.addAnnotation(Pattern.class, this::verRegex); + validator.addAnnotation(ClassName.class, this::verClassName); + validator.addAnnotation(Valid.class, this::verCascade); + } + + /** + * Performs validation of all annotated fields found within the class. + * + * @param result validation results are added here + * @param object object whose fields are to be validated + * @param clazz class, within the object's hierarchy, to be examined for fields to be + * verified + */ + private void validateFields(BeanValidationResult result, Object object, Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + var validator = makeFieldValidator(clazz, field); + validator.validateField(result, object); + } + } + + /** + * Verifies that the value is not null. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verNotNull(BeanValidationResult result, String fieldName, Object value) { + if (value == null) { + result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, "is null"); + return false; + } + + return true; + } + + /** + * Verifies that the value is not blank. Note: this does not verify that the + * value is not {@code null}. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verNotBlank(BeanValidationResult result, String fieldName, Object value) { + if (value instanceof String && StringUtils.isBlank(value.toString())) { + result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, "is blank"); + return false; + } + + return true; + } + + /** + * Verifies that the value has the specified number of elements. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param annot annotation against which the value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verSize(BeanValidationResult result, String fieldName, Size annot, Object value) { + int size; + if (value instanceof Collection) { + size = ((Collection) value).size(); + + } else if (value instanceof Map) { + size = ((Map) value).size(); + + } else { + return true; + } + + + if (size < annot.min()) { + result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, + "minimum number of elements: " + annot.min()); + return false; + } + + return true; + } + + /** + * Verifies that the value matches a regular expression. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param annot annotation against which the value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verRegex(BeanValidationResult result, String fieldName, Pattern annot, Object value) { + try { + if (value instanceof String && com.google.re2j.Pattern.matches(annot.regexp(), value.toString())) { + return true; + } + + } catch (RuntimeException e) { + logger.warn("validation error for regular expression: {}", annot.regexp(), e); + } + + result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, + "does not match regular expression " + annot.regexp()); + return false; + } + + /** + * Verifies that the value is <= the minimum value. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param annot annotation against which the value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verMax(BeanValidationResult result, String fieldName, Max annot, Object value) { + if (!(value instanceof Number)) { + return true; + } + + Number num = (Number) value; + if (num instanceof Integer || num instanceof Long) { + if (num.longValue() <= annot.value()) { + return true; + } + + } else if (num instanceof Float || num instanceof Double) { + if (num.doubleValue() <= annot.value()) { + return true; + } + + } else { + return true; + } + + result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, + "exceeds the maximum value: " + annot.value()); + return false; + } + + /** + * Verifies that the value is >= the minimum value. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param annot annotation against which the value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verMin(BeanValidationResult result, String fieldName, Min annot, Object value) { + return verMin(result, fieldName, annot.value(), value); + } + + /** + * Verifies that the value is >= the minimum value. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param min minimum against which the value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verMin(BeanValidationResult result, String fieldName, long min, Object value) { + if (!(value instanceof Number)) { + return true; + } + + Number num = (Number) value; + if (num instanceof Integer || num instanceof Long) { + if (num.longValue() >= min) { + return true; + } + + } else if (num instanceof Float || num instanceof Double) { + if (num.doubleValue() >= min) { + return true; + } + + } else { + return true; + } + + result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, + "is below the minimum value: " + min); + return false; + } + + /** + * Verifies that the value is a valid class name. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verClassName(BeanValidationResult result, String fieldName, Object value) { + if (!(value instanceof String)) { + return true; + } + + try { + Class.forName(value.toString()); + return true; + + } catch (final ClassNotFoundException exp) { + result.addResult(fieldName, value, ValidationStatus.INVALID, "class is not in the classpath"); + return false; + } + } + + /** + * Verifies that the value is valid by recursively invoking + * {@link #validateTop(String, Object)}. + * + * @param result where to add the validation result + * @param fieldName field whose value is being verified + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verCascade(BeanValidationResult result, String fieldName, Object value) { + if (value == null || value instanceof Collection || value instanceof Map) { + return true; + } + + BeanValidationResult result2 = (value instanceof ParameterGroup parameterGroup ? parameterGroup.validate() + : validateTop(fieldName, value)); + + if (result2.isClean()) { + return true; + } + + result.addResult(result2); + + return result2.isValid(); + } + + /** + * Validates the items in a collection. + * + * @param result where to add the validation result + * @param fieldName name of the field containing the collection + * @param itemValidator validator for individual items within the list + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verCollection(BeanValidationResult result, String fieldName, ValueValidator itemValidator, + Object value) { + + if (!(value instanceof Collection)) { + return true; + } + + Collection list = (Collection) value; + + var result2 = new BeanValidationResult(fieldName, value); + var count = 0; + for (Object item : list) { + itemValidator.validateValue(result2, String.valueOf(count++), item); + } + + if (result2.isClean()) { + return true; + } + + result.addResult(result2); + return false; + } + + /** + * Validates the items in a Map. + * + * @param result where to add the validation result + * @param fieldName name of the field containing the map + * @param keyValidator validator for an individual key within the Map entry + * @param valueValidator validator for an individual value within the Map entry + * @param value value to be verified + * @return {@code true} if the next check should be performed, {@code false} otherwise + */ + public boolean verMap(BeanValidationResult result, String fieldName, ValueValidator keyValidator, + ValueValidator valueValidator, Object value) { + + if (!(value instanceof Map)) { + return true; + } + + Map map = (Map) value; + + var result2 = new BeanValidationResult(fieldName, value); + + for (Entry entry : map.entrySet()) { + String name = getEntryName(entry); + + var result3 = new BeanValidationResult(name, entry); + keyValidator.validateValue(result3, "key", entry.getKey()); + valueValidator.validateValue(result3, "value", entry.getValue()); + + if (!result3.isClean()) { + result2.addResult(result3); + } + } + + if (result2.isClean()) { + return true; + } + + result.addResult(result2); + return false; + } + + /** + * Gets a name for an entry. + * + * @param entry entry whose name is to be determined + * @return a name for the entry + */ + protected String getEntryName(Map.Entry entry) { + var key = entry.getKey(); + if (key == null) { + return ""; + } + + return key.toString(); + } + + /** + * Makes a field validator. + * + * @param clazz class containing the field + * @param field field of interest + * @return a validator for the given field + */ + protected FieldValidator makeFieldValidator(Class clazz, Field field) { + return new FieldValidator(this, clazz, field); + } + + /** + * Translates a value to something printable, for use by + * {@link ObjectValidationResult}. This default method simply returns the original + * value. + * + * @param value value to be translated + * @return the translated value + */ + public Object xlate(Object value) { + return value; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/FieldValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/FieldValidator.java new file mode 100644 index 000000000..d441c286b --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/FieldValidator.java @@ -0,0 +1,292 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import com.google.gson.annotations.SerializedName; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Map; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.parameters.annotations.NotNull; + +/** + * Validator of the contents of a field, supporting the parameter annotations. + */ +public class FieldValidator extends ValueValidator { + + /** + * {@code True} if there is a field-level annotation, {@code false} otherwise. + */ + @Getter + @Setter(AccessLevel.PROTECTED) + private boolean fieldAnnotated = false; + + /** + * Class containing the field of interest. + */ + private final Class clazz; + + /** + * Field of interest. + */ + private final Field field; + + /** + * Name of the field when serialized (i.e., as the client would know it). + */ + private final String serializedName; + + /** + * Method to retrieve the field's value. + */ + private Method accessor; + + + /** + * Constructs the object. + * + * @param validator provider of validation methods + * @param clazz class containing the field + * @param field field whose value is to be validated + */ + public FieldValidator(BeanValidator validator, Class clazz, Field field) { + this.clazz = clazz; + this.field = field; + + String fieldName = field.getName(); + if (fieldName.contains("$")) { + serializedName = fieldName; + return; + } + + SerializedName serAnnot = field.getAnnotation(SerializedName.class); + serializedName = (serAnnot != null ? serAnnot.value() : fieldName); + + validator.addValidators(this); + addListValidator(validator); + addMapValidator(validator); + + if (checkers.isEmpty()) { + // has no annotations - nothing to check + return; + } + + // verify the field type is of interest + int mod = field.getModifiers(); + if (Modifier.isStatic(mod)) { + classOnly(clazz.getName() + "." + fieldName + " is annotated but the field is static"); + checkers.clear(); + return; + } + + // get the field's "getter" method + accessor = getAccessor(clazz, fieldName); + if (accessor == null) { + classOnly(clazz.getName() + "." + fieldName + " is annotated but has no \"get\" method"); + checkers.clear(); + return; + } + + // determine if null is allowed + if (field.getAnnotation(NotNull.class) != null || clazz.getAnnotation(NotNull.class) != null) { + setNullAllowed(false); + } + } + + /** + * Adds validators for the individual items within a collection, if the field is a + * collection. + * + * @param validator provider of validation methods + */ + private void addListValidator(BeanValidator validator) { + if (!Collection.class.isAssignableFrom(field.getType())) { + return; + } + + var tannot = field.getAnnotatedType(); + if (!(tannot instanceof AnnotatedParameterizedType)) { + return; + } + + AnnotatedType[] targs = ((AnnotatedParameterizedType) tannot).getAnnotatedActualTypeArguments(); + if (targs.length != 1) { + return; + } + + var itemValidator = new ItemValidator(validator, targs[0]); + if (itemValidator.isEmpty()) { + return; + } + + checkers.add((result, fieldName, value) -> validator.verCollection(result, fieldName, itemValidator, value)); + } + + /** + * Adds validators for the individual entries within a map, if the field is a map. + * + * @param validator provider of validation methods + */ + private void addMapValidator(BeanValidator validator) { + if (!Map.class.isAssignableFrom(field.getType())) { + return; + } + + var tannot = field.getAnnotatedType(); + if (!(tannot instanceof AnnotatedParameterizedType)) { + return; + } + + AnnotatedType[] targs = ((AnnotatedParameterizedType) tannot).getAnnotatedActualTypeArguments(); + if (targs.length != 2) { + return; + } + + var keyValidator = new ItemValidator(validator, targs[0]); + var valueValidator = new ItemValidator(validator, targs[1]); + if (keyValidator.isEmpty() && valueValidator.isEmpty()) { + return; + } + + checkers.add((result, fieldName, value) -> validator.verMap(result, fieldName, keyValidator, valueValidator, + value)); + } + + /** + * Performs validation of a single field. + * + * @param result validation results are added here + * @param object object whose field is to be validated + */ + public void validateField(BeanValidationResult result, Object object) { + if (isEmpty()) { + // has no annotations - nothing to check + return; + } + + // get the value + Object value = getValue(object, accessor); + + validateValue(result, serializedName, value); + } + + /** + * Gets the value from the object using the accessor function. + * + * @param object object whose value is to be retrieved + * @param accessor "getter" method + * @return the object's value + */ + private Object getValue(Object object, Method accessor) { + try { + return accessor.invoke(object); + + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalArgumentException(clazz.getName() + "." + field.getName() + " accessor threw an exception", + e); + } + } + + /** + * Throws an exception if there are field-level annotations. + * + * @param exceptionMessage exception message + */ + private void classOnly(String exceptionMessage) { + if (isFieldAnnotated()) { + throw new IllegalArgumentException(exceptionMessage); + } + } + + /** + * Gets an annotation from the field or the class. + * + * @param annotClass annotation class of interest + * @return the annotation, or {@code null} if neither the field nor the class has the + * desired annotation + */ + @Override + public T getAnnotation(Class annotClass) { + + // field annotation takes precedence over class annotation + var annot = field.getAnnotation(annotClass); + if (annot != null) { + setFieldAnnotated(true); + return annot; + } + + return clazz.getAnnotation(annotClass); + } + + /** + * Gets an accessor method for the given field. + * + * @param clazz class whose methods are to be searched + * @param fieldName field whose "getter" is to be identified + * @return the field's "getter" method, or {@code null} if it is not found + */ + private Method getAccessor(Class clazz, String fieldName) { + var capname = StringUtils.capitalize(fieldName); + var accessor2 = getMethod(clazz, "get" + capname); + if (accessor2 != null) { + return accessor2; + } + + return getMethod(clazz, "is" + capname); + } + + /** + * Gets the "getter" method having the specified name. + * + * @param clazz class whose methods are to be searched + * @param methodName name of the method of interest + * @return the method, or {@code null} if it is not found + */ + private Method getMethod(Class clazz, String methodName) { + for (Method method : clazz.getMethods()) { + if (methodName.equals(method.getName()) && validMethod(method)) { + return method; + } + } + + return null; + } + + /** + * Determines if a method is a valid "getter". + * + * @param method method to be checked + * @return {@code true} if the method is a valid "getter", {@code false} otherwise + */ + private boolean validMethod(Method method) { + int mod = method.getModifiers(); + return !(Modifier.isStatic(mod) || method.getReturnType() == void.class || method.getParameterCount() != 0); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ItemValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ItemValidator.java new file mode 100644 index 000000000..44b70cdf6 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ItemValidator.java @@ -0,0 +1,71 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; + +/** + * Validator of an "item", which is typically found in a collection, or the key or value + * components of an entry in a Map. + */ +public class ItemValidator extends ValueValidator { + private final AnnotatedType annotatedType; + + /** + * Constructs the object. + * + * @param validator provider of validation methods + * @param annotatedType a type having validation annotations to be + * applied to the item + */ + public ItemValidator(BeanValidator validator, AnnotatedType annotatedType) { + this(validator, annotatedType, true); + } + + /** + * Constructs the object. + * + * @param validator provider of validation methods + * @param annotatedType a type having validation annotations to be + * applied to the item + * @param addValidators {@code true} if to add validators + */ + public ItemValidator(BeanValidator validator, AnnotatedType annotatedType, boolean addValidators) { + this.annotatedType = annotatedType; + + if (addValidators) { + validator.addValidators(this); + } + } + + /** + * Gets an annotation from the field or the class. + * + * @param annotClass annotation class of interest + * @return the annotation, or {@code null} if the {@link #annotatedType} does + * not contain the desired annotation + */ + @Override + public T getAnnotation(Class annotClass) { + return annotatedType.getAnnotation(annotClass); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ObjectValidationResult.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ObjectValidationResult.java new file mode 100644 index 000000000..af1884a3a --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ObjectValidationResult.java @@ -0,0 +1,67 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +/** + * This class holds the result of the validation of an object within a bean. + */ +public class ObjectValidationResult extends ValidationResultImpl { + + /** + * Constructs the object. + * + * @param name name of the object of this result + * @param object object being validated + */ + public ObjectValidationResult(String name, Object object) { + super(name, object); + } + + /** + * Constructs the object. + * + * @param name name of the object of this result + * @param object object being validated + * @param status result status + * @param message result message + */ + public ObjectValidationResult(String name, Object object, ValidationStatus status, String message) { + super(name, object, status, message); + } + + /** + * Gets the validation result. + * + * @param initialIndentation the result indentation + * @param subIndentation the indentation to use on sub parts of the result output + * @param showClean output information on clean fields + * @return the result + */ + @Override + public String getResult(final String initialIndentation, final String subIndentation, final boolean showClean) { + if (!showClean && getStatus() == ValidationStatus.CLEAN) { + return null; + } + + return initialIndentation + "item \"" + getName() + "\" value \"" + getObject() + "\" " + getStatus() + ", " + + getMessage() + '\n'; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterConstants.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterConstants.java new file mode 100644 index 000000000..300d49c7e --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterConstants.java @@ -0,0 +1,43 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * This static class holds the values of constants for parameter handling. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ParameterConstants { + // Indentation is 0 on the left and 2 for each level of hierarchy + public static final String DEFAULT_INITIAL_RESULT_INDENTATION = ""; + public static final String DEFAULT_RESULT_INDENTATION = " "; + + // By default we do not show validation results for parameters that are validated as clean + public static final boolean DO_NOT_SHOW_CLEAN_RESULTS = false; + + // Messages for clean validations + public static final String PARAMETER_GROUP_HAS_STATUS_MESSAGE = "parameter group has status "; + public static final String PARAMETER_GROUP_MAP_HAS_STATUS_MESSAGE = "parameter group map has status "; + public static final String PARAMETER_HAS_STATUS_MESSAGE = "parameter has status "; +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroup.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroup.java new file mode 100644 index 000000000..c26b7b44d --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroup.java @@ -0,0 +1,61 @@ +/*- + * ============LICENSE_START========================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================== + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +/** + * This interface acts as a base interface for all parameter groups in the ONAP Policy Framework. All parameter group + * POJOs are implementations of the parameter group interface and can be used with the {@link ParameterService}. + * + * @author Liam Fallon (liam.fallon@ericsson.com) + */ +public interface ParameterGroup { + /** + * Get the group name. + * + * @return the group name + */ + String getName(); + + /** + * Set the group name. + * + * @param name the group name + */ + void setName(final String name); + + /** + * Validate parameters. + * + * @return the result of the parameter validation + */ + BeanValidationResult validate(); + + /** + * Check if the parameters are valid. + * + * @return true if the parameters are valid + */ + default boolean isValid() { + return validate().getStatus().isValid(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroupImpl.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroupImpl.java new file mode 100644 index 000000000..8a987bdd2 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroupImpl.java @@ -0,0 +1,49 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; + +/** + * Implementation of a parameter group. + */ +@NotNull +@NotBlank +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ParameterGroupImpl implements ParameterGroup { + /** + * Group name. + */ + private String name; + + @Override + public BeanValidationResult validate() { + return new BeanValidator().validateTop(getClass().getSimpleName(), this); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResult.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResult.java new file mode 100644 index 000000000..e60174101 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResult.java @@ -0,0 +1,89 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +/** + * This interface defines the result of a parameter validation. + */ +public interface ValidationResult { + /** + * Gets the name of the entity being validated. + * + * @return the name + */ + String getName(); + + /** + * Gets the status of validation. + * + * @return the status + */ + ValidationStatus getStatus(); + + /** + * Checks if the result is valid. + * + * @return true, if is valid + */ + default boolean isValid() { + return getStatus().isValid(); + } + + /** + * Checks if the result is clean. + * + * @return true, if is clean + */ + default boolean isClean() { + return getStatus().isClean(); + } + + /** + * Gets the validation result. + * + * @return the full validation result + */ + default String getResult() { + return getResult( + ParameterConstants.DEFAULT_INITIAL_RESULT_INDENTATION, + ParameterConstants.DEFAULT_RESULT_INDENTATION, + ParameterConstants.DO_NOT_SHOW_CLEAN_RESULTS); + } + + /** + * Gets the validation result. + * + * @param initialIndentation the indentation to use on the main result output + * @param subIndentation the indentation to use on sub parts of the result output + * @param showClean output information on clean fields + * @return the result + */ + String getResult(final String initialIndentation, final String subIndentation, final boolean showClean); + + /** + * Set a validation result. + * + * @param status The validation status the field is receiving + * @param message The validation message explaining the validation status + */ + void setResult(final ValidationStatus status, final String message); +} \ No newline at end of file diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResultImpl.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResultImpl.java new file mode 100644 index 000000000..458988185 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResultImpl.java @@ -0,0 +1,108 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Basic implementation of a ValidationResult. + */ +@Getter +@AllArgsConstructor +public abstract class ValidationResultImpl implements ValidationResult { + public static final String ITEM_HAS_STATUS_MESSAGE = "item has status "; + + /** + * Name of the object of this result. + */ + private final String name; + + /** + * Object being validated. + */ + private final Object object; + + /** + * Validation status of this object. + */ + private ValidationStatus status = ValidationStatus.CLEAN; + + /** + * Validation message. + */ + private String message = ITEM_HAS_STATUS_MESSAGE + status.toString(); + + + /** + * Constructs the object. + * + * @param name name of the object of this result + * @param object object being validated + */ + protected ValidationResultImpl(String name, Object object) { + this.name = name; + this.object = object; + } + + /** + * Validates that the value is not {@code null}. + * + * @return {@code true} if the value is not null, {@code false} otherwise + */ + public boolean validateNotNull() { + if (object == null) { + setResult(ValidationStatus.INVALID, "is null"); + return false; + + } else { + return true; + } + } + + /** + * Set the validation result status. On a sequence of calls, the most serious + * validation status is recorded, assuming the status enum ordinals increase in order + * of severity. + * + * @param status validation status the bean is receiving + */ + public void setResult(final ValidationStatus status) { + setResult(status, ITEM_HAS_STATUS_MESSAGE + status.toString()); + } + + /** + * Set the validation result status. On a sequence of calls, the most serious + * validation status is recorded, assuming the status enum ordinals increase in order + * of severity. + * + * @param status the validation status + * @param message the validation message explaining the validation status + */ + @Override + public void setResult(final ValidationStatus status, final String message) { + if (this.status.ordinal() < status.ordinal()) { + this.status = status; + this.message = message; + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationStatus.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationStatus.java new file mode 100644 index 000000000..8692506f0 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationStatus.java @@ -0,0 +1,44 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +public enum ValidationStatus { + CLEAN, + OBSERVATION, + WARNING, + INVALID; + + /** + * The result of a validation is valid unless the status is INVALID. + * @return true if the validation has passed + */ + public boolean isValid() { + return !this.equals(INVALID); + } + + /** + * Check if the validation was clean. + * @return true if the validation is clean + */ + public boolean isClean() { + return this.equals(CLEAN); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValueValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValueValidator.java new file mode 100644 index 000000000..faf410087 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/ValueValidator.java @@ -0,0 +1,146 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.onap.policy.common.parameters.annotations.NotNull; + +/** + * Validator of a value. + *

+ * Note: this currently does not support Min/Max validation of "short" or "byte"; these + * annotations are simply ignored for these types. + */ +@NoArgsConstructor +public class ValueValidator { + + /** + * {@code True} if the value is allowed to be {@code null}, {@code false} otherwise. + * Subclasses are expected to set this, typically based on the validation annotations + * associated with the value. + */ + @Getter + @Setter(AccessLevel.PROTECTED) + private boolean nullAllowed = true; + + /** + * Predicates to invoke to validate an object. + *

+ * Note: each predicate is expected to return {@code true} if the next check is + * allowed to proceed, {@code false} otherwise. In addition, if {@link #nullAllowed} + * is {@code true}, then the predicates must be prepared to deal with a {@code null} + * Object as their input parameter. + */ + protected List checkers = new ArrayList<>(10); + + /** + * Determines if the validator has anything to check. + * + * @return {@code true} if the validator is empty (i.e., has nothing to check) + */ + public boolean isEmpty() { + return checkers.isEmpty(); + } + + /** + * Performs validation of a single field. + * + * @param result validation results are added here + * @param fieldName field whose value is being verified + * @param value value to be validated + */ + protected void validateValue(BeanValidationResult result, String fieldName, Object value) { + + if (value == null && isNullAllowed()) { + // value is null and null is allowed - just return + return; + } + + for (Checker checker : checkers) { + if (!checker.test(result, fieldName, value)) { + // invalid - don't bother with additional checks + return; + } + } + } + + /** + * Looks for an annotation at the class or field level. If an annotation is found at + * either the field or class level, then it adds a verifier to + * {@link ValueValidator#checkers}. + * + * @param annotClass class of annotation to find + * @param checker function to validate the value + */ + public void addAnnotation(Class annotClass, Checker checker) { + var annot = getAnnotation(annotClass); + if (annot != null) { + checkers.add(checker); + + if (annotClass == NotNull.class) { + setNullAllowed(false); + } + } + } + + /** + * Looks for an annotation at the class or field level. If an annotation is found at + * either the field or class level, then it adds a verifier to + * {@link ValueValidator#checkers}. + * + * @param annotClass class of annotation to find + * @param checker function to validate the value + */ + public void addAnnotation(Class annotClass, CheckerWithAnnot checker) { + var annot = getAnnotation(annotClass); + if (annot != null) { + checkers.add((result, fieldName, value) -> checker.test(result, fieldName, annot, value)); + } + } + + /** + * Gets an annotation from the field or the class. The default method simply returns + * {@code null}. + * + * @param annotClass annotation class of interest + * @return the annotation, or {@code null} if neither the field nor the class has the + * desired annotation + */ + public T getAnnotation(Class annotClass) { + return null; + } + + // functions to validate a value extracted from a field + + public static interface Checker { + boolean test(BeanValidationResult result, String fieldName, Object value); + } + + public static interface CheckerWithAnnot { + boolean test(BeanValidationResult result, String fieldName, T annotation, Object value); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/ClassName.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/ClassName.java new file mode 100644 index 000000000..14d76fd7e --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/ClassName.java @@ -0,0 +1,37 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Indicates that a field (i.e., String) identifies the name of a class in the classpath. + */ +@Retention(RUNTIME) +@Target({FIELD, TYPE_USE}) +public @interface ClassName { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Max.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Max.java new file mode 100644 index 000000000..f28fd2cfb --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Max.java @@ -0,0 +1,40 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({FIELD, TYPE_USE}) +public @interface Max { + + /** + * The maximum value allowed. + * + * @return the maximum value allowed + */ + long value(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Min.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Min.java new file mode 100644 index 000000000..305d98104 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Min.java @@ -0,0 +1,40 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({FIELD, TYPE_USE}) +public @interface Min { + + /** + * The minimum value allowed. + * + * @return the minimum value allowed + */ + long value(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotBlank.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotBlank.java new file mode 100644 index 000000000..0744beb58 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotBlank.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Indicates that a field (i.e., String) may not be empty. + */ +@Retention(RUNTIME) +@Target({TYPE, FIELD, TYPE_USE}) +public @interface NotBlank { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotNull.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotNull.java new file mode 100644 index 000000000..813560051 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotNull.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Indicates that a field may not be null. + */ +@Retention(RUNTIME) +@Target({TYPE, FIELD, TYPE_USE}) +public @interface NotNull { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java new file mode 100644 index 000000000..91a74d70f --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java @@ -0,0 +1,38 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({FIELD, TYPE_USE}) +public @interface Pattern { + + /** + * Regular expression to be matched. + */ + String regexp(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Size.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Size.java new file mode 100644 index 000000000..160e0124a --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Size.java @@ -0,0 +1,43 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Indicates the size of a Map or Collection. + */ +@Retention(RUNTIME) +@Target({FIELD, TYPE_USE}) +public @interface Size { + + /** + * The minimum size allowed. + * + * @return the minimum size allowed + */ + int min(); +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java new file mode 100644 index 000000000..227a726fb --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({TYPE, FIELD, TYPE_USE}) +public @interface Valid { + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/topic/BusTopicParams.java b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/BusTopicParams.java new file mode 100644 index 000000000..66ac57c5a --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/BusTopicParams.java @@ -0,0 +1,338 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + * Modifications Copyright (C) 2018-2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.topic; + +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +/** + * Member variables of this Params class are as follows. + * + *

servers Kafka servers + * topic Kafka Topic to be monitored + * apiKey Kafka API Key (optional) + * apiSecret Kafka API Secret (optional) + * consumerGroup kafka Reader Consumer Group + * consumerInstance Kafka Reader Instance + * fetchTimeout kafka fetch timeout + * fetchLimit Kafka fetch limit + * environment DME2 Environment + * aftEnvironment DME2 AFT Environment + * partner DME2 Partner + * latitude DME2 Latitude + * longitude DME2 Longitude + * additionalProps Additional properties to pass to DME2 + * useHttps does connection use HTTPS? + * allowTracing is message tracing allowed? + * allowSelfSignedCerts are self-signed certificates allow + */ +@Getter +@Setter +public class BusTopicParams { + + private int port; + private List servers; + private Map additionalProps; + private String topic; + private String effectiveTopic; + private String apiKey; + private String apiSecret; + private String consumerGroup; + private String consumerInstance; + private int fetchTimeout; + private int fetchLimit; + private boolean useHttps; + private boolean allowTracing; + private boolean allowSelfSignedCerts; + private boolean managed; + + private String userName; + private String password; + private String environment; + private String aftEnvironment; + private String partner; + private String latitude; + private String longitude; + private String partitionId; + private String clientName; + private String hostname; + private String basePath; + @Getter + private String serializationProvider; + + public static TopicParamsBuilder builder() { + return new TopicParamsBuilder(); + } + + /** + * Methods to Check if the property is INVALID. + */ + + boolean isEnvironmentInvalid() { + return StringUtils.isBlank(environment); + } + + boolean isAftEnvironmentInvalid() { + return StringUtils.isBlank(aftEnvironment); + } + + boolean isLatitudeInvalid() { + return StringUtils.isBlank(latitude); + } + + boolean isLongitudeInvalid() { + return StringUtils.isBlank(longitude); + } + + public boolean isConsumerInstanceInvalid() { + return StringUtils.isBlank(consumerInstance); + } + + public boolean isConsumerGroupInvalid() { + return StringUtils.isBlank(consumerGroup); + } + + public boolean isClientNameInvalid() { + return StringUtils.isBlank(clientName); + } + + boolean isPartnerInvalid() { + return StringUtils.isBlank(partner); + } + + boolean isServersInvalid() { + return (servers == null || servers.isEmpty() + || (servers.size() == 1 && ("".equals(servers.get(0))))); + } + + public boolean isTopicInvalid() { + return StringUtils.isBlank(topic); + } + + public boolean isPartitionIdInvalid() { + return StringUtils.isBlank(partitionId); + } + + public boolean isHostnameInvalid() { + return StringUtils.isBlank(hostname); + } + + public boolean isPortInvalid() { + return (getPort() <= 0 || getPort() >= 65535); + } + + /** + * Methods to Check if the property is Valid. + */ + + boolean isApiKeyValid() { + return StringUtils.isNotBlank(apiKey); + } + + boolean isApiSecretValid() { + return StringUtils.isNotBlank(apiSecret); + } + + boolean isUserNameValid() { + return StringUtils.isNotBlank(userName); + } + + boolean isPasswordValid() { + return StringUtils.isNotBlank(password); + } + + public boolean isAdditionalPropsValid() { + return additionalProps != null; + } + + public void setEffectiveTopic(String effectiveTopic) { + this.effectiveTopic = topicToLowerCase(effectiveTopic); + } + + public void setTopic(String topic) { + this.topic = topicToLowerCase(topic); + } + + public String getEffectiveTopic() { + return topicToLowerCase(effectiveTopic); + } + + public String getTopic() { + return topicToLowerCase(topic); + } + + private String topicToLowerCase(String topic) { + return (topic == null || topic.isEmpty()) ? topic : topic.toLowerCase(); + } + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class TopicParamsBuilder { + + final BusTopicParams params = new BusTopicParams(); + + public TopicParamsBuilder servers(List servers) { + this.params.servers = servers; + return this; + } + + public TopicParamsBuilder topic(String topic) { + this.params.setTopic(topic); + return this; + } + + public TopicParamsBuilder effectiveTopic(String effectiveTopic) { + this.params.setEffectiveTopic(effectiveTopic); + return this; + } + + public TopicParamsBuilder apiKey(String apiKey) { + this.params.apiKey = apiKey; + return this; + } + + public TopicParamsBuilder apiSecret(String apiSecret) { + this.params.apiSecret = apiSecret; + return this; + } + + public TopicParamsBuilder consumerGroup(String consumerGroup) { + this.params.consumerGroup = consumerGroup; + return this; + } + + public TopicParamsBuilder consumerInstance(String consumerInstance) { + this.params.consumerInstance = consumerInstance; + return this; + } + + public TopicParamsBuilder fetchTimeout(int fetchTimeout) { + this.params.fetchTimeout = fetchTimeout; + return this; + } + + public TopicParamsBuilder fetchLimit(int fetchLimit) { + this.params.fetchLimit = fetchLimit; + return this; + } + + public TopicParamsBuilder useHttps(boolean useHttps) { + this.params.useHttps = useHttps; + return this; + } + + public TopicParamsBuilder allowTracing(boolean allowTracing) { + this.params.allowTracing = allowTracing; + return this; + } + + public TopicParamsBuilder allowSelfSignedCerts(boolean allowSelfSignedCerts) { + this.params.allowSelfSignedCerts = allowSelfSignedCerts; + return this; + } + + public TopicParamsBuilder userName(String userName) { + this.params.userName = userName; + return this; + } + + public TopicParamsBuilder password(String password) { + this.params.password = password; + return this; + } + + public TopicParamsBuilder environment(String environment) { + this.params.environment = environment; + return this; + } + + public TopicParamsBuilder aftEnvironment(String aftEnvironment) { + this.params.aftEnvironment = aftEnvironment; + return this; + } + + public TopicParamsBuilder partner(String partner) { + this.params.partner = partner; + return this; + } + + public TopicParamsBuilder latitude(String latitude) { + this.params.latitude = latitude; + return this; + } + + public TopicParamsBuilder longitude(String longitude) { + this.params.longitude = longitude; + return this; + } + + public TopicParamsBuilder additionalProps(Map additionalProps) { + this.params.additionalProps = additionalProps; + return this; + } + + public TopicParamsBuilder partitionId(String partitionId) { + this.params.partitionId = partitionId; + return this; + } + + public BusTopicParams build() { + return params; + } + + public TopicParamsBuilder managed(boolean managed) { + this.params.managed = managed; + return this; + } + + public TopicParamsBuilder hostname(String hostname) { + this.params.hostname = hostname; + return this; + } + + public TopicParamsBuilder clientName(String clientName) { + this.params.clientName = clientName; + return this; + } + + public TopicParamsBuilder port(int port) { + this.params.port = port; + return this; + } + + public TopicParamsBuilder basePath(String basePath) { + this.params.basePath = basePath; + return this; + } + + public TopicParamsBuilder serializationProvider(String serializationProvider) { + this.params.serializationProvider = serializationProvider; + return this; + } + } +} + diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameterGroup.java b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameterGroup.java new file mode 100644 index 000000000..c9d5e3e8a --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameterGroup.java @@ -0,0 +1,92 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2024 Nordix Foundation. + * Modifications Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.topic; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.parameters.BeanValidationResult; +import org.onap.policy.common.parameters.ParameterGroupImpl; +import org.onap.policy.common.parameters.ValidationStatus; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; + +/** + * Class to hold all parameters needed for topic properties. + * + * @author Ajith Sreekumar (ajith.sreekumar@est.tech) + */ +@NotNull +@NotBlank +@Getter +@Setter +public class TopicParameterGroup extends ParameterGroupImpl { + + private List topicSources; + private List topicSinks; + + public TopicParameterGroup() { + super(TopicParameterGroup.class.getSimpleName()); + } + + /** + * {@inheritDoc}. + */ + @Override + public BeanValidationResult validate() { + BeanValidationResult result = super.validate(); + if (result.isValid()) { + var errorMsg = new StringBuilder(); + StringBuilder missingSourceParams = checkMissingMandatoryParams(topicSources); + if (!missingSourceParams.isEmpty()) { + errorMsg.append(missingSourceParams.append("missing in topicSources. ")); + } + StringBuilder missingSinkParams = checkMissingMandatoryParams(topicSinks); + if (!missingSinkParams.isEmpty()) { + errorMsg.append(missingSinkParams.append("missing in topicSinks.")); + } + + if (!errorMsg.isEmpty()) { + errorMsg.insert(0, "Mandatory parameters are missing. "); + result.setResult(ValidationStatus.INVALID, errorMsg.toString()); + } + } + return result; + } + + private StringBuilder checkMissingMandatoryParams(List topicParametersList) { + var missingParams = new StringBuilder(); + for (TopicParameters topicParameters : topicParametersList) { + if (StringUtils.isBlank(topicParameters.getTopic())) { + missingParams.append("topic, "); + } + if (StringUtils.isBlank(topicParameters.getTopicCommInfrastructure())) { + missingParams.append("topicCommInfrastructure, "); + } + if (null == topicParameters.getServers() || topicParameters.getServers().isEmpty()) { + missingParams.append("servers, "); + } + } + return missingParams; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameters.java b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameters.java new file mode 100644 index 000000000..2b8bfbdb2 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameters.java @@ -0,0 +1,47 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2024 Nordix Foundation. + * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.topic; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; + +/** + * Class to hold topic details such as name, server and topicCommInfrastructure. + * + * @author Ajith Sreekumar (ajith.sreekumar@est.tech) + */ +@NotNull +@NotBlank +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public class TopicParameters extends BusTopicParams { + private String topicCommInfrastructure; + + public TopicParameters() { + // this defaults to true + setManaged(true); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupConstraint.java b/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupConstraint.java new file mode 100644 index 000000000..c73d135a0 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupConstraint.java @@ -0,0 +1,57 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021, 2023 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = ParameterGroupValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ParameterGroupConstraint { + + /** + * Get error Message. + * + * @return error Message + */ + String message() default "validation error(s) on parameters"; + + /** + * Get groups. + * + * @return Class arrays + */ + Class[] groups() default {}; + + /** + * Get payload. + * + * @return Class arrays + */ + Class[] payload() default {}; +} diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupValidator.java new file mode 100644 index 000000000..024ca1391 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupValidator.java @@ -0,0 +1,42 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021, 2023 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.onap.policy.common.parameters.BeanValidationResult; +import org.onap.policy.common.parameters.ParameterGroup; + +public class ParameterGroupValidator implements ConstraintValidator { + + @Override + public boolean isValid(ParameterGroup value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + final BeanValidationResult result = value.validate(); + if (!result.isValid()) { + context.buildConstraintViolationWithTemplate(result.getMessage()).addConstraintViolation(); + } + return result.isValid(); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategy.java b/policy-common/src/main/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategy.java new file mode 100644 index 000000000..d9a7e97c0 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategy.java @@ -0,0 +1,39 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2023 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.spring.utils; + +import java.io.Serial; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ImplicitJoinColumnNameSource; +import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl; + +public class CustomImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl { + + @Serial + private static final long serialVersionUID = 8666774028328486896L; + + @Override + public Identifier determineJoinColumnName(ImplicitJoinColumnNameSource source) { + String name = source.getReferencedColumnName().getText(); + return toIdentifier(name, source.getBuildingContext()); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverter.java b/policy-common/src/main/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverter.java new file mode 100644 index 000000000..5fa026089 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverter.java @@ -0,0 +1,108 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Bell Canada. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.spring.utils; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import org.onap.policy.common.utils.coder.YamlJsonTranslator; +import org.springframework.core.GenericTypeResolver; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractGenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.lang.Nullable; + +/** + * Custom converter to marshal/unmarshall data structured with YAML media type. + */ +public class YamlHttpMessageConverter extends AbstractGenericHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private static final YamlJsonTranslator TRANSLATOR = new YamlJsonTranslator(); + + public YamlHttpMessageConverter() { + super(new MediaType("application", "yaml")); + setDefaultCharset(DEFAULT_CHARSET); + } + + @Override + public final Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) + throws IOException { + return readResolved(GenericTypeResolver.resolveType(type, contextClass), inputMessage); + } + + @Override + protected final Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + return readResolved(clazz, inputMessage); + } + + private Object readInternal(Type resolvedType, Reader reader) { + Class clazz = (Class) resolvedType; + return TRANSLATOR.fromYaml(reader, clazz); + } + + @Override + protected final void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException { + try (var writer = getWriter(outputMessage)) { + writeInternal(object, writer); + writer.flush(); + } catch (Exception ex) { + throw new HttpMessageNotWritableException("Could not write YAML: " + ex.getMessage(), ex); + } + } + + private void writeInternal(Object object, Writer writer) { + TRANSLATOR.toYaml(writer, object); + } + + private Object readResolved(Type resolvedType, HttpInputMessage inputMessage) throws IOException { + try (var reader = getReader(inputMessage)) { + return readInternal(resolvedType, reader); + } catch (Exception ex) { + throw new HttpMessageNotReadableException("Could not read YAML: " + ex.getMessage(), ex, inputMessage); + } + } + + private static Reader getReader(HttpInputMessage inputMessage) throws IOException { + return new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders())); + } + + private static Writer getWriter(HttpOutputMessage outputMessage) throws IOException { + return new OutputStreamWriter(outputMessage.getBody(), getCharset(outputMessage.getHeaders())); + } + + private static Charset getCharset(HttpHeaders headers) { + MediaType contentType = headers.getContentType(); + Charset charset = (contentType == null ? null : contentType.getCharset()); + return (charset != null ? charset : DEFAULT_CHARSET); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/Coder.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/Coder.java new file mode 100644 index 000000000..3049a5c21 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/coder/Coder.java @@ -0,0 +1,169 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/** + * JSON encoder and decoder. + */ +public interface Coder { + + /** + * Converts an object/POJO to an object of the given type. + * + * @param desired type + * @param source source object + * @param clazz class of the desired object type + * @return the converted object + * @throws CoderException if an error occurs + */ + default T convert(S source, Class clazz) throws CoderException { + if (source == null) { + return null; + + } else if (clazz == source.getClass()) { + // same class - just cast it + return clazz.cast(source); + + } else if (clazz == String.class) { + // target is a string - just encode the source + return (clazz.cast(encode(source))); + + } else if (source.getClass() == String.class) { + // source is a string - just decode it + return decode(source.toString(), clazz); + + } else { + // do it the long way: encode to a string and then decode the string + return decode(encode(source), clazz); + } + } + + /** + * Encodes an object into json. + * + * @param object object to be encoded + * @return a json string representing the object + * @throws CoderException if an error occurs + */ + String encode(Object object) throws CoderException; + + /** + * Encodes an object into json, optionally making it "pretty". + * + * @param object object to be encoded + * @param pretty {@code true} if it should be encoded as "pretty" json, {@code false} + * otherwise + * @return a json string representing the object + * @throws CoderException if an error occurs + */ + String encode(Object object, boolean pretty) throws CoderException; + + /** + * Encodes an object into json, writing to the given target. + * + * @param target target to which to write the encoded json + * @param object object to be encoded + * @throws CoderException if an error occurs + */ + void encode(Writer target, Object object) throws CoderException; + + /** + * Encodes an object into json, writing to the given target. + * + * @param target target to which to write the encoded json + * @param object object to be encoded + * @throws CoderException if an error occurs + */ + void encode(OutputStream target, Object object) throws CoderException; + + /** + * Encodes an object into json, writing to the given target. + * + * @param target target to which to write the encoded json + * @param object object to be encoded + * @throws CoderException if an error occurs + */ + void encode(File target, Object object) throws CoderException; + + /** + * Decodes json into an object. + * + * @param json json string to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json string + * @throws CoderException if an error occurs + */ + T decode(String json, Class clazz) throws CoderException; + + /** + * Decodes json into an object, reading it from the given source. + * + * @param source source from which to read the json string to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json string + * @throws CoderException if an error occurs + */ + T decode(Reader source, Class clazz) throws CoderException; + + /** + * Decodes json into an object, reading it from the given source. + * + * @param source source from which to read the json string to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json string + * @throws CoderException if an error occurs + */ + T decode(InputStream source, Class clazz) throws CoderException; + + /** + * Decodes json into an object, reading it from the given source. + * + * @param source source from which to read the json string to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json string + * @throws CoderException if an error occurs + */ + T decode(File source, Class clazz) throws CoderException; + + /** + * Converts an object/POJO to a standard object. + * + * @param object object to be converted + * @return a new standard object representing the original object + * @throws CoderException if an error occurs + */ + StandardCoderObject toStandard(Object object) throws CoderException; + + /** + * Converts a standard object to an object/POJO. + * + * @param sco the standard object to be converted + * @return a new object represented by the standard object + * @throws CoderException if an error occurs + */ + T fromStandard(StandardCoderObject sco, Class clazz) throws CoderException; +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/CoderException.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/CoderException.java new file mode 100644 index 000000000..8390d1758 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/coder/CoderException.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import java.io.Serial; + +/** + * Exceptions generated by coders. + */ +public class CoderException extends Exception { + + @Serial + private static final long serialVersionUID = 1L; + + public CoderException() { + super(); + } + + public CoderException(String message) { + super(message); + } + + public CoderException(Throwable cause) { + super(cause); + } + + public CoderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java new file mode 100644 index 000000000..834a85048 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java @@ -0,0 +1,393 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.onap.policy.common.gson.DoubleConverter; +import org.onap.policy.common.gson.GsonMessageBodyHandler; + +/** + * JSON encoder and decoder using the "standard" mechanism, which is currently gson. + */ +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class StandardCoder implements Coder { + + /** + * Gson object used to encode and decode messages. + */ + private static final Gson GSON_STD; + + /** + * Gson object used to encode messages in "pretty" format. + */ + private static final Gson GSON_STD_PRETTY; + + static { + GsonBuilder builder = GsonMessageBodyHandler.configBuilder( + new GsonBuilder().registerTypeAdapter(StandardCoderObject.class, new StandardTypeAdapter())); + + GSON_STD = builder.create(); + GSON_STD_PRETTY = builder.setPrettyPrinting().create(); + } + + /** + * Gson object used to encode and decode messages. + */ + protected final Gson gson; + + /** + * Gson object used to encode messages in "pretty" format. + */ + protected final Gson gsonPretty; + + /** + * Constructs the object. + */ + public StandardCoder() { + this(GSON_STD, GSON_STD_PRETTY); + } + + @Override + public T convert(S source, Class clazz) throws CoderException { + if (source == null) { + return null; + + } else if (clazz == source.getClass()) { + // same class - just cast it + return clazz.cast(source); + + } else if (clazz == String.class) { + // target is a string - just encode the source + return (clazz.cast(encode(source))); + + } else if (source.getClass() == String.class) { + // source is a string - just decode it + return decode(source.toString(), clazz); + + } else { + /* + * Do it the long way: encode to a tree and then decode the tree. This entire + * method could have been left out and the default Coder.convert() used + * instead, but this should perform slightly better as it only uses a + * JsonElement as the intermediate data structure, while Coder.convert() goes + * all the way to a String as the intermediate data structure. + */ + try { + return fromJson(toJsonTree(source), clazz); + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + } + + @Override + public String encode(Object object) throws CoderException { + return encode(object, false); + } + + @Override + public String encode(Object object, boolean pretty) throws CoderException { + try { + if (pretty) { + return toPrettyJson(object); + + } else { + return toJson(object); + } + + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + @Override + public void encode(Writer target, Object object) throws CoderException { + try { + toJson(target, object); + + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + @Override + public void encode(OutputStream target, Object object) throws CoderException { + try { + var wtr = makeWriter(target); + toJson(wtr, object); + + // flush, but don't close + wtr.flush(); + + } catch (RuntimeException | IOException e) { + throw new CoderException(e); + } + } + + @Override + public void encode(File target, Object object) throws CoderException { + try (var wtr = makeWriter(target)) { + toJson(wtr, object); + + // no need to flush or close here + + } catch (RuntimeException | IOException e) { + throw new CoderException(e); + } + } + + @Override + public T decode(String json, Class clazz) throws CoderException { + try { + return fromJson(json, clazz); + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + @Override + public T decode(Reader source, Class clazz) throws CoderException { + try { + return fromJson(source, clazz); + + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + @Override + public T decode(InputStream source, Class clazz) throws CoderException { + try { + return fromJson(makeReader(source), clazz); + + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + @Override + public T decode(File source, Class clazz) throws CoderException { + try (var input = makeReader(source)) { + return fromJson(input, clazz); + + } catch (RuntimeException | IOException e) { + throw new CoderException(e); + } + } + + /** + * Encodes the object as "pretty" json. + * + * @param object object to be encoded + * @return the encoded object + */ + protected String toPrettyJson(Object object) { + return gsonPretty.toJson(object); + } + + @Override + public StandardCoderObject toStandard(Object object) throws CoderException { + try { + return new StandardCoderObject(gson.toJsonTree(object)); + + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + @Override + public T fromStandard(StandardCoderObject sco, Class clazz) throws CoderException { + try { + return gson.fromJson(sco.getData(), clazz); + + } catch (RuntimeException e) { + throw new CoderException(e); + } + } + + // the remaining methods are wrappers that can be overridden by junit tests + + /** + * Makes a writer for the given file. + * + * @param target file of interest + * @return a writer for the file + * @throws FileNotFoundException if the file cannot be created + */ + protected Writer makeWriter(File target) throws FileNotFoundException { + return makeWriter(new FileOutputStream(target)); + } + + /** + * Makes a writer for the given stream. + * + * @param target stream of interest + * @return a writer for the stream + */ + protected Writer makeWriter(OutputStream target) { + return new OutputStreamWriter(target, StandardCharsets.UTF_8); + } + + /** + * Makes a reader for the given file. + * + * @param source file of interest + * @return a reader for the file + * @throws FileNotFoundException if the file does not exist + */ + protected Reader makeReader(File source) throws FileNotFoundException { + return makeReader(new FileInputStream(source)); + } + + /** + * Makes a reader for the given stream. + * + * @param source stream of interest + * @return a reader for the stream + */ + protected Reader makeReader(InputStream source) { + return new InputStreamReader(source, StandardCharsets.UTF_8); + } + + /** + * Encodes an object into a json tree, without catching exceptions. + * + * @param object object to be encoded + * @return a json element representing the object + */ + protected JsonElement toJsonTree(Object object) { + return gson.toJsonTree(object); + } + + /** + * Encodes an object into json, without catching exceptions. + * + * @param object object to be encoded + * @return a json string representing the object + */ + protected String toJson(Object object) { + return gson.toJson(object); + } + + /** + * Encodes an object into json, without catching exceptions. + * + * @param target target to which to write the encoded json + * @param object object to be encoded + */ + protected void toJson(Writer target, Object object) { + gson.toJson(object, object.getClass(), target); + } + + /** + * Decodes a json element into an object, without catching exceptions. + * + * @param json json element to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json element + */ + protected T fromJson(JsonElement json, Class clazz) { + return convertFromDouble(clazz, gson.fromJson(json, clazz)); + } + + /** + * Decodes a json string into an object, without catching exceptions. + * + * @param json json string to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json string + */ + protected T fromJson(String json, Class clazz) { + return convertFromDouble(clazz, gson.fromJson(json, clazz)); + } + + /** + * Decodes a json string into an object, without catching exceptions. + * + * @param source source from which to read the json string to be decoded + * @param clazz class of object to be decoded + * @return the object represented by the given json string + */ + protected T fromJson(Reader source, Class clazz) { + return convertFromDouble(clazz, gson.fromJson(source, clazz)); + } + + /** + * Converts a value from Double to Integer/Long, walking the value's contents if it's + * a List/Map. Only applies if the specified class refers to the Object class. + * Otherwise, it leaves the value unchanged. + * + * @param clazz class of object to be decoded + * @param value value to be converted + * @return the converted value + */ + protected T convertFromDouble(Class clazz, T value) { + if (clazz != Object.class && !Map.class.isAssignableFrom(clazz) && !List.class.isAssignableFrom(clazz)) { + return value; + } + + return clazz.cast(DoubleConverter.convertFromDouble(value)); + } + + /** + * Adapter for standard objects. + */ + @AllArgsConstructor + protected static class StandardTypeAdapter extends TypeAdapter { + + /** + * Used to read/write a JsonElement. + */ + private static final TypeAdapter elementAdapter = new Gson().getAdapter(JsonElement.class); + + @Override + public void write(JsonWriter out, StandardCoderObject value) throws IOException { + elementAdapter.write(out, value.getData()); + } + + @Override + public StandardCoderObject read(JsonReader in) throws IOException { + return new StandardCoderObject(elementAdapter.read(in)); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoderObject.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoderObject.java new file mode 100644 index 000000000..5402f37b4 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoderObject.java @@ -0,0 +1,128 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import com.google.gson.JsonElement; +import java.io.Serial; +import java.io.Serializable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Object type used by the {@link StandardCoder}. Different serialization tools have + * different "standard objects". For instance, GSON uses {@link JsonElement}. This class + * wraps that object so that it can be used without exposing the object, itself. + */ +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class StandardCoderObject implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Data wrapped by this. + */ + /* + * this should not be transient, but since it isn't serializable, we're stuck with it + * until there's time to address the issue + */ + @Getter(AccessLevel.PROTECTED) + private final transient JsonElement data; + + /** + * Constructs the object. + */ + public StandardCoderObject() { + data = null; + } + + /** + * Gets a field's value from this object, traversing the object hierarchy. + * + * @param fields field hierarchy. These may be strings, identifying fields within the + * object, or Integers, identifying an index within an array + * @return the field value or {@code null} if the field does not exist or is not a primitive + */ + public String getString(Object... fields) { + + JsonElement jel = data; + + for (Object field : fields) { + if (jel == null) { + return null; + } + + if (field instanceof String) { + jel = getFieldFromObject(jel, field.toString()); + + } else if (field instanceof Integer) { + jel = getItemFromArray(jel, (int) field); + + } else { + throw new IllegalArgumentException("subscript is not a string or integer: " + field); + } + } + + return (jel != null && jel.isJsonPrimitive() ? jel.getAsString() : null); + } + + /** + * Gets an item from an object. + * + * @param element object from which to extract the item + * @param field name of the field from which to extract the item + * @return the item, or {@code null} if the element is not an object or if the field does not exist + */ + protected JsonElement getFieldFromObject(JsonElement element, String field) { + if (!element.isJsonObject()) { + return null; + } + + return element.getAsJsonObject().get(field); + } + + /** + * Gets an item from an array. + * + * @param element array from which to extract the item + * @param index index of the item to extract + * @return the item, or {@code null} if the element is not an array or if the index is out of bounds + */ + protected JsonElement getItemFromArray(JsonElement element, int index) { + if (index < 0) { + throw new IllegalArgumentException("subscript is invalid: " + index); + } + + if (!element.isJsonArray()) { + return null; + } + + var array = element.getAsJsonArray(); + + if (index >= array.size()) { + return null; + } + + return array.get(index); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java new file mode 100644 index 000000000..8ee2e81da --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java @@ -0,0 +1,71 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import java.io.Reader; +import java.io.Writer; + +/** + * YAML encoder and decoder using the "standard" mechanism, which is currently gson. + * All the methods perform conversion to/from YAML (instead of JSON). + */ +public class StandardYamlCoder extends StandardCoder { + private final YamlJsonTranslator translator; + + /** + * Constructs the object. + */ + public StandardYamlCoder() { + translator = new YamlJsonTranslator(gson) { + @Override + protected T convertFromDouble(Class clazz, T value) { + return StandardYamlCoder.this.convertFromDouble(clazz, value); + } + }; + } + + @Override + protected String toPrettyJson(Object object) { + // YAML is already "pretty" + return toJson(object); + } + + @Override + protected String toJson(Object object) { + return translator.toYaml(object); + } + + @Override + protected void toJson(Writer target, Object object) { + translator.toYaml(target, object); + } + + @Override + protected T fromJson(String yaml, Class clazz) { + return translator.fromYaml(yaml, clazz); + } + + @Override + protected T fromJson(Reader source, Class clazz) { + return translator.fromYaml(source, clazz); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java new file mode 100644 index 000000000..ffd9d0520 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java @@ -0,0 +1,340 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import lombok.AllArgsConstructor; +import org.onap.policy.common.gson.InstantTypeAdapter; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.emitter.Emitter; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.resolver.Resolver; +import org.yaml.snakeyaml.serializer.Serializer; + +/** + * YAML-JSON translator. The methods may throw either of the runtime exceptions, + * YAMLException or JsonSyntaxException. + *

+ * Note: if the invoker wishes Double to be converted to Integer/Long when type + * Object.class is requested, then a Gson object must be used that will perform the + * translation. In addition, the {@link #convertFromDouble(Class, Object)} method should + * be overridden with an appropriate conversion method. + */ +@AllArgsConstructor +public class YamlJsonTranslator { + + /** + * Object to be used to translate between YAML and JsonElement. + */ + private final Gson gson; + + /** + * Constructs the object. + */ + public YamlJsonTranslator() { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(Instant.class, new InstantTypeAdapter()); + gson = builder.create(); + } + + /** + * Translates a POJO into a YAML String. + * + * @param object POJO to be translated + * @return YAML representing the original object + */ + public String toYaml(Object object) { + var output = new StringWriter(); + toYaml(output, object); + return output.toString(); + } + + /** + * Serializes a POJO to a writer, as YAML. + * + * @param target target writer + * @param object POJO to be translated + */ + public void toYaml(Writer target, Object object) { + var dumper = new DumperOptions(); + var serializer = new Serializer(new Emitter(target, dumper), new Resolver(), dumper, null); + + try { + serializer.open(); + serializer.serialize(makeYaml(toJsonTree(object))); + serializer.close(); + + } catch (IOException e) { + throw new YAMLException(e); + } + } + + /** + * Translates a POJO into a JsonElement. + * + * @param object POJO to be translated + * @return a JsonElement representing the original object + */ + protected JsonElement toJsonTree(Object object) { + return gson.toJsonTree(object); + } + + /** + * Translates a YAML string to a POJO. + * + * @param yaml YAML string to be translated + * @param clazz class of POJO to be created + * @return a POJO representing the original YAML + */ + public T fromYaml(String yaml, Class clazz) { + return fromYaml(new StringReader(yaml), clazz); + } + + /** + * Translates a YAML string, read from a reader, into a POJO. + * + * @param source source of the YAML string to be translated + * @param clazz class of POJO to be created + * @return a POJO representing the YAML read from the reader + */ + public T fromYaml(Reader source, Class clazz) { + var node = new Yaml().compose(source); + return fromJson(makeJson(node), clazz); + } + + /** + * Translates a JsonElement to a POJO of the given class. + * + * @param jel element to be translated + * @param clazz class of POJO to be created + * @return a POJO representing the original element + */ + protected T fromJson(JsonElement jel, Class clazz) { + return convertFromDouble(clazz, gson.fromJson(jel, clazz)); + } + + /** + * Converts a value from Double to Integer/Long, walking the value's contents if it's + * a List/Map. Only applies if the specified class refers to the Object class. + * Otherwise, it leaves the value unchanged. + *

+ * The default method simply returns the original value. + * + * @param clazz class of object to be decoded + * @param value value to be converted + * @return the converted value + */ + protected T convertFromDouble(Class clazz, T value) { + return value; + } + + /** + * Converts an arbitrary gson element into a corresponding Yaml node. + * + * @param jel gson element to be converted + * @return a yaml node corresponding to the element + */ + protected Node makeYaml(JsonElement jel) { + if (jel.isJsonArray()) { + return makeYamlSequence((JsonArray) jel); + + } else if (jel.isJsonObject()) { + return makeYamlMap((JsonObject) jel); + + } else if (jel.isJsonPrimitive()) { + return makeYamlPrim((JsonPrimitive) jel); + + } else { + return new ScalarNode(Tag.NULL, "", null, null, DumperOptions.ScalarStyle.PLAIN); + } + } + + /** + * Converts an arbitrary gson array into a corresponding Yaml sequence. + * + * @param jel gson element to be converted + * @return a yaml node corresponding to the element + */ + protected SequenceNode makeYamlSequence(JsonArray jel) { + List nodes = new ArrayList<>(jel.size()); + jel.forEach(item -> nodes.add(makeYaml(item))); + + return new SequenceNode(Tag.SEQ, true, nodes, null, null, DumperOptions.FlowStyle.AUTO); + } + + /** + * Converts an arbitrary gson object into a corresponding Yaml map. + * + * @param jel gson element to be converted + * @return a yaml node corresponding to the element + */ + protected MappingNode makeYamlMap(JsonObject jel) { + List nodes = new ArrayList<>(jel.size()); + + for (Entry entry : jel.entrySet()) { + Node key = new ScalarNode(Tag.STR, entry.getKey(), null, null, DumperOptions.ScalarStyle.PLAIN); + Node value = makeYaml(entry.getValue()); + + nodes.add(new NodeTuple(key, value)); + } + + return new MappingNode(Tag.MAP, true, nodes, null, null, DumperOptions.FlowStyle.AUTO); + } + + /** + * Converts an arbitrary gson primitive into a corresponding Yaml scalar. + * + * @param jel gson element to be converted + * @return a yaml node corresponding to the element + */ + protected ScalarNode makeYamlPrim(JsonPrimitive jel) { + Tag tag; + if (jel.isNumber()) { + Class clazz = jel.getAsNumber().getClass(); + + if (clazz == Double.class || clazz == Float.class) { + tag = Tag.FLOAT; + + } else { + tag = Tag.INT; + } + + } else if (jel.isBoolean()) { + tag = Tag.BOOL; + + } else { + // treat anything else as a string + tag = Tag.STR; + } + + return new ScalarNode(tag, jel.getAsString(), null, null, DumperOptions.ScalarStyle.PLAIN); + } + + /** + * Converts an arbitrary Yaml node into a corresponding gson element. + * + * @param node node to be converted + * @return a gson element corresponding to the node + */ + protected JsonElement makeJson(Node node) { + if (node instanceof MappingNode mappingNode) { + return makeJsonObject(mappingNode); + + } else if (node instanceof SequenceNode sequenceNode) { + return makeJsonArray(sequenceNode); + + } else { + return makeJsonPrim((ScalarNode) node); + } + + // yaml doesn't appear to use anchor nodes when decoding so ignore them for now + } + + /** + * Converts a Yaml sequence into a corresponding gson array. + * + * @param node node to be converted + * @return a gson element corresponding to the node + */ + protected JsonArray makeJsonArray(SequenceNode node) { + List nodes = node.getValue(); + + var array = new JsonArray(nodes.size()); + nodes.forEach(subnode -> array.add(makeJson(subnode))); + + return array; + } + + /** + * Converts a Yaml map into a corresponding gson object. + * + * @param node node to be converted + * @return a gson element corresponding to the node + */ + protected JsonObject makeJsonObject(MappingNode node) { + var obj = new JsonObject(); + + for (NodeTuple tuple : node.getValue()) { + var key = tuple.getKeyNode(); + String skey = ((ScalarNode) key).getValue(); + + obj.add(skey, makeJson(tuple.getValueNode())); + } + + return obj; + } + + /** + * Converts a Yaml scalar into a corresponding gson primitive. + * + * @param node node to be converted + * @return a gson element corresponding to the node + */ + protected JsonElement makeJsonPrim(ScalarNode node) { + try { + var tag = node.getTag(); + + if (tag == Tag.INT) { + return new JsonPrimitive(Long.valueOf(node.getValue())); + + } else if (tag == Tag.FLOAT) { + return new JsonPrimitive(Double.valueOf(node.getValue())); + + } else if (tag == Tag.BOOL) { + return new JsonPrimitive(Boolean.valueOf(node.getValue())); + + } else if (tag == Tag.NULL) { + return JsonNull.INSTANCE; + + } else { + // treat anything else as a string + return new JsonPrimitive(node.getValue()); + } + + } catch (NumberFormatException ex) { + // just treat it as a string + return new JsonPrimitive(node.getValue()); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonSerializer.java b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonSerializer.java new file mode 100644 index 000000000..db9c1c7ec --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonSerializer.java @@ -0,0 +1,37 @@ +/*- + * ============LICENSE_START======================================================= + * policy-management + * ================================================================================ + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import java.io.IOException; + +/** + * Gson serializer, providing stub implementation of "read". + * + * @param type of object that this serializes + */ +public abstract class GsonSerializer extends TypeAdapter { + @Override + public T read(JsonReader in) throws IOException { + throw new UnsupportedOperationException("read from pseudo TypeAdapter"); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtils.java new file mode 100644 index 000000000..1d131aed0 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtils.java @@ -0,0 +1,314 @@ +/*- + * ============LICENSE_START======================================================= + * policy-management + * ================================================================================ + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.re2j.Pattern; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.jexl3.JexlBuilder; +import org.apache.commons.jexl3.JexlContext; +import org.apache.commons.jexl3.JexlEngine; +import org.apache.commons.jexl3.MapContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utilities used to test encoding and decoding of Policy objects. + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class GsonTestUtils { + + private static final Logger logger = LoggerFactory.getLogger(GsonTestUtils.class); + + /** + * Matches script items, of the form ${xxx}, within text. + */ + private static final Pattern SCRIPT_PAT = Pattern.compile("\\$\\{([^}]+)\\}"); + + /** + * Engine used to interpolate strings before they're compared. + */ + private static JexlEngine engineInstance = null; + + /** + * Used to encode and decode an object via gson. + */ + private Gson gson; + + /** + * Constructs the object. + */ + public GsonTestUtils() { + GsonTestUtils other = new GsonTestUtilsBuilder().build(); + + gson = other.gson; + } + + /** + * Serializes and then deserializes an object using gson. + * + * @param object the object to be serialized + * @param clazz the class of object to deserialize + * @return the deserialized object + */ + public T gsonRoundTrip(T object, Class clazz) { + String sgson = gsonEncode(object); + return gson.fromJson(sgson, clazz); + } + + /** + * Encodes an object using gson and then compares it to the expected value, after + * sorting the elements. The class name is used to find the json file, whose contents + * is interpolated (i.e., script elements, of the form ${obj.xxx}, are expanded). + * + * @param object the object to be encoded + * @param expected the expected value + */ + public void compareGson(Object object, Class expected) { + compareGson(object, new File(expected.getSimpleName() + ".json")); + } + + /** + * Encodes an object using gson and then compares it to the expected value, after + * sorting the elements. The content of the file is interpolated (i.e., script + * elements, of the form ${obj.xxx}, are expanded). + * + * @param object the object to be encoded + * @param expected the expected value + */ + public void compareGson(Object object, File expected) { + // file is not required to have a full path - find it via getResource() + var url = object.getClass().getResource(expected.getName()); + if (url == null) { + throw new JsonParseException(new FileNotFoundException(expected.getName())); + } + + String expectedText; + try { + expectedText = readFile(new File(url.getFile())); + + } catch (IOException e) { + throw new JsonParseException("error reading: " + expected, e); + } + + compareGson(object, expectedText); + } + + /** + * Encodes an object using gson and then compares it to the expected value, after + * sorting the elements. The expected value is interpolated (i.e., script elements, of + * the form ${obj.xxx}, are expanded). + * + * @param object the object to be encoded + * @param expected the expected value + */ + public void compareGson(Object object, String expected) { + String result = applyScripts(expected, object); + compareGson(object, gson.fromJson(result, JsonElement.class)); + } + + /** + * Encodes an object using gson and then compares it to the expected value, after + * sorting the elements. + * + * @param object the object to be encoded + * @param expected the expected value + */ + public void compareGson(Object object, JsonElement expected) { + String sgson = gsonEncode(object); + + JsonElement gsonjo = reorder(gson.fromJson(sgson, JsonElement.class)); + JsonElement expjo = reorder(expected); + + /* + * As this method is only used within junit tests, it is OK to use assert calls, + * thus sonar is disabled. + */ + assertEquals(expjo.toString(), gsonjo.toString()); // NOSONAR + } + + /** + * Reads the content of a file. + * @param file file to read + * @return the content of the file + * @throws IOException if an error occurs + */ + protected String readFile(File file) throws IOException { + return Files.readString(file.toPath()); + } + + + /** + * Interpolates script elements, of the form ${obj.xxx}, within some text. The script + * is evaluated via javascript, where "obj" references the object used by each script + * element. + * + * @param object object to be used by the script + * @param text text to be evaluated + * @return the text, after interpolating the script elements + */ + public String applyScripts(String text, Object object) { + var mat = SCRIPT_PAT.matcher(text); + if (!mat.find()) { + // contains no script elements - just return it as is + return text; + } + + // bind the object to the variable, "obj" + JexlEngine eng = getEngine(); + JexlContext context = new MapContext(); + context.set("obj", object); + + // work our way through the text, interpolating script elements as we go + var bldr = new StringBuilder(); + var ilast = 0; + mat.reset(); + while (mat.find(ilast)) { + // append segment that appears between last match and this + int inext = mat.start(); + bldr.append(text, ilast, inext); + + // next match begins after the current match + ilast = mat.end(); + + // interpolate the script + String script = mat.group(1); + /* + * Note: must use "eng" instead of "engineInstance" to ensure that we use + * the same engine that's associated with the bindings. + */ + Object result = eng.createExpression(script).evaluate(context); + bldr.append(result == null ? "null" : result.toString()); + } + + // append final segment + bldr.append(text.substring(ilast)); + + return bldr.toString(); + } + + /** + * Gets the script engine instance. + * + * @return the script engine + */ + private static JexlEngine getEngine() { + if (engineInstance == null) { + // race condition here, but it's ok to overwrite with a new engine + engineInstance = new JexlBuilder().create(); + } + + return engineInstance; + } + + /** + * Encodes an object using gson. + * + * @param object the object to be encoded + * @return the encoded object + */ + public String gsonEncode(Object object) { + String sgson = gson.toJson(object); + logger.debug("gson={}", sgson); + return sgson; + } + + /** + * Recursively re-orders a json object, arranging the keys alphabetically and removing + * null items. + * + * @param jsonObj object from which nulls are to be removed + * @return a new object, without the null items + */ + public JsonObject reorder(JsonObject jsonObj) { + var newjo = new JsonObject(); + + // sort the keys before copying to the new object + List> sortedSet = new ArrayList<>(jsonObj.entrySet()); + sortedSet.sort(Entry.comparingByKey()); + + for (Entry ent : sortedSet) { + JsonElement val = ent.getValue(); + if (val.isJsonNull()) { + continue; + } + + newjo.add(ent.getKey(), reorder(val)); + } + + return newjo; + } + + /** + * Recursively re-orders a json array, arranging the keys alphabetically and removing + * null items. + * + * @param jsonArray array from which nulls are to be removed + * @return a new array, with null items removed from all elements + */ + public JsonArray reorder(JsonArray jsonArray) { + var newarr = new JsonArray(); + for (JsonElement ent : jsonArray) { + newarr.add(reorder(ent)); + } + + return newarr; + } + + /** + * Recursively re-orders a json element, arranging the keys alphabetically and + * removing null items. + * + * @param jsonEl element from which nulls are to be removed + * @return a new element, with null items removed + */ + public JsonElement reorder(JsonElement jsonEl) { + if (jsonEl == null) { + return null; + + } else if (jsonEl.isJsonObject()) { + return reorder(jsonEl.getAsJsonObject()); + + } else if (jsonEl.isJsonArray()) { + return reorder(jsonEl.getAsJsonArray()); + + } else { + return jsonEl; + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilder.java b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilder.java new file mode 100644 index 000000000..dcf277509 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilder.java @@ -0,0 +1,61 @@ +/*- + * ============LICENSE_START======================================================= + * policy-management + * ================================================================================ + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.gson; + +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapterFactory; +import org.onap.policy.common.gson.JacksonHandler; + +/** + * Used to builder a utility class. + */ +public class GsonTestUtilsBuilder { + private final GsonBuilder gsonBldr; + + /** + * Constructs the object. + */ + public GsonTestUtilsBuilder() { + gsonBldr = new GsonBuilder(); + + // register jackson behaviors with the builder + JacksonHandler.configBuilder(gsonBldr); + } + + /** + * Builds the utility. + * + * @return a new utility + */ + public GsonTestUtils build() { + return new GsonTestUtils(gsonBldr.create()); + } + + /** + * Adds gson support for serializing a mock of a class. + * + * @param clazz mocked class to be supported + * @param sgson gson serializer + */ + protected void addMock(Class clazz, TypeAdapterFactory sgson) { + gsonBldr.registerTypeAdapterFactory(sgson); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/network/NetworkUtil.java b/policy-common/src/main/java/org/onap/policy/common/utils/network/NetworkUtil.java new file mode 100644 index 000000000..6698d7cf1 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/network/NetworkUtil.java @@ -0,0 +1,185 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.network; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.UUID; +import javax.net.ssl.TrustManager; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.net.util.TrustManagerUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Network Utilities. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class NetworkUtil { + + public static final Logger logger = LoggerFactory.getLogger(NetworkUtil.class.getName()); + + /** + * IPv4 Wildcard IP address. + */ + public static final String IPV4_WILDCARD_ADDRESS = "0.0.0.0"; + + + /** + * A trust manager that always trusts certificates. + */ + private static final TrustManager[] ALWAYS_TRUST_MANAGER = { TrustManagerUtils.getAcceptAllTrustManager() }; + + /** + * Allocates an available port on which a server may listen. + * + * @return an available port + * @throws IOException if a socket cannot be created + */ + public static int allocPort() throws IOException { + return allocPort((InetSocketAddress) null); + } + + /** + * Allocates an available port on which a server may listen. + * + * @param hostName the server's host name + * @return an available port + * @throws IOException if a socket cannot be created + */ + public static int allocPort(String hostName) throws IOException { + return allocPort(new InetSocketAddress(hostName, 0)); + } + + /** + * Allocates an available port on which a server may listen. + * + * @param hostAddr the server's host address on which to listen + * @return an available port + * @throws IOException if a socket cannot be created + */ + public static int allocPort(InetSocketAddress hostAddr) throws IOException { + /* + * The socket is only used to find an unused address for a new server. As a + * result, it poses no security risk, thus the sonar issue can be ignored. + */ + try (ServerSocket socket = new ServerSocket()) { // NOSONAR + socket.bind(hostAddr); + + return socket.getLocalPort(); + } + } + + /** + * Gets a trust manager that accepts all certificates. + * + * @return a trust manager that accepts all certificates + */ + public static TrustManager[] getAlwaysTrustingManager() { + return ALWAYS_TRUST_MANAGER; + } + + /** + * try to connect to $host:$port $retries times while we are getting connection failures. + * + * @param host host + * @param port port + * @param retries number of attempts + * @return true is port is open, false otherwise + * @throws InterruptedException if execution has been interrupted + */ + public static boolean isTcpPortOpen(String host, int port, int retries, long interval) + throws InterruptedException { + var retry = 0; + while (retry < retries) { + /* + * As with the server socket, this is only used to see if the port is open, + * thus the sonar issue can be ignored. + */ + try (Socket s = new Socket(host, port)) { // NOSONAR + logger.debug("{}:{} connected - retries={} interval={}", host, port, retries, interval); + return true; + } catch (final IOException e) { + retry++; + logger.trace("{}:{} connected - retries={} interval={}", host, port, retries, interval, e); + Thread.sleep(interval); + } + } + + logger.warn("{}:{} closed = retries={} interval={}", host, port, retries, interval); + return false; + } + + /** + * Gets host name. + * + * @return host name + */ + public static String getHostname() { + + String hostname = System.getenv("HOSTNAME"); + if (hostname != null && !hostname.isEmpty()) { + return hostname; + } + + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + logger.warn("cannot resolve local hostname", e); + /* continue */ + } + + return "localhost"; + } + + /** + * Gets host's IP. + * + * @return host IP + */ + public static String getHostIp() { + + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + logger.warn("cannot resolve local hostname", e); + /* continue */ + } + + return "127.0.0.1"; + } + + /** + * Generates a globally unique name, typically for use in PDP messages, to uniquely + * identify a PDP (or PAP), regardless on what cluster it resides. + * + * @param prefix text to be prepended to the generated value + * @return a globally unique name + */ + public static String genUniqueName(String prefix) { + return prefix + "-" + UUID.randomUUID(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/properties/PropertyUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/properties/PropertyUtils.java new file mode 100644 index 000000000..7155323bd --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/properties/PropertyUtils.java @@ -0,0 +1,123 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.properties; + +import java.util.Properties; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +/** + * Utilities for extracting property values and converting them to other types. + */ +@AllArgsConstructor +public class PropertyUtils { + /** + * Properties on which to work. + */ + private Properties properties; + + /** + * Prefix to prepend to property names. + */ + private String prefix; + + /** + * Function to invoke if a property value is invalid. + */ + private TriConsumer invalidHandler; + + /** + * Gets a string property. + * + * @param propName name of the property whose value is to be retrieved + * @param defaultValue value to use if the property value is empty or does not exist + * @return the property's value + */ + public String getString(String propName, String defaultValue) { + String propValue = getProperty(propName); + return (StringUtils.isBlank(propValue) ? defaultValue : propValue); + } + + /** + * Gets a boolean property. + * + * @param propName name of the property whose value is to be retrieved + * @param defaultValue value to use if the property value is empty or does not exist + * @return the property's value + */ + public boolean getBoolean(String propName, boolean defaultValue) { + String propValue = getProperty(propName); + + if (!StringUtils.isBlank(propValue)) { + return Boolean.parseBoolean(propValue); + } + + return defaultValue; + } + + /** + * Gets an integer property. + * + * @param propName name of the property whose value is to be retrieved + * @param defaultValue value to use if the property value is empty or does not exist + * @return the property's value + */ + public int getInteger(String propName, int defaultValue) { + String propValue = getProperty(propName); + + if (!StringUtils.isBlank(propValue)) { + try { + return Integer.parseInt(propValue); + + } catch (NumberFormatException nfe) { + invalidHandler.accept(getFullName(propName), propValue, nfe); + } + } + + return defaultValue; + } + + + /** + * Gets a property's value. + * + * @param propName name of the property whose value is to be retrieved + * @return the property's value, or {@code null} if it does not exist + */ + private String getProperty(String propName) { + return properties.getProperty(getFullName(propName)); + } + + /** + * Gets the full property name, with the prefix prepended. + * + * @param propName property name, without the prefix + * @return the full property name + */ + private String getFullName(String propName) { + return prefix + propName; + } + + @FunctionalInterface + public static interface TriConsumer { + public void accept(A propName, B propValue, C exception); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/resources/ResourceUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/resources/ResourceUtils.java new file mode 100644 index 000000000..3ee062f10 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/resources/ResourceUtils.java @@ -0,0 +1,308 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2020, 2023 Nordix Foundation. + * Modifications Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.resources; + +import com.google.re2j.Pattern; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is common utility class with static methods for handling Java resources on the class path. It is an abstract + * class to prevent any direct instantiation and private constructor to prevent extending this class. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ResourceUtils { + // Get a reference to the logger + private static final Logger LOGGER = LoggerFactory.getLogger(ResourceUtils.class); + + private static final Pattern SLASH_PAT = Pattern.compile("/"); + + // Resource types + private static final String FILE_PROTOCOL = "file"; + private static final String JAR_PROTOCOL = "jar"; + + /** + * Method to resolve a resource; the local file system is checked first and then the class path is checked. + * + * @param resourceName The resource name + * @return A URL to a resource + */ + public static URL getUrl4Resource(final String resourceName) { + // Check the local fine system first + final var urlToResource = getLocalFile(resourceName); + + // Check if this is a local file + if (urlToResource != null) { + return urlToResource; + } else { + // Resort to the class path + return getUrlResource(resourceName); + } + } + + /** + * Method to return a resource as a string. The resource can be on the local file system or in the class path. The + * resource is resolved and loaded into a string. + * + * @param resourceName The resource name + * @return A string containing the resource + */ + public static String getResourceAsString(final String resourceName) { + // Get the resource as a stream, we'll convert it to a string then + // Read the stream contents, closing when done + try (var resourceStream = getResourceAsStream(resourceName)) { + if (resourceStream == null) { + return null; + } + return IOUtils.toString(resourceStream, StandardCharsets.UTF_8); + } catch (final IOException e) { + LOGGER.debug("error reading resource stream {}", resourceName, e); + return null; + } + } + + /** + * Method to return a resource as a stream. The resource can be on the local file system or in the class path. The + * resource is resolved and returned as a stream. + * + * @param resourceName The resource name + * @return A stream attached to the resource + */ + public static InputStream getResourceAsStream(final String resourceName) { + // Find a URL to the resource first + final var urlToResource = getUrl4Resource(resourceName); + + // Check if the resource exists + if (urlToResource == null) { + // No resource found + LOGGER.debug("could not find resource \"{}\" : ", resourceName); + return null; + } + + // Read the resource into a string + try { + return urlToResource.openStream(); + } catch (final IOException e) { + // Any of many IO exceptions such as the resource is a directory + LOGGER.debug("error attaching resource {}", resourceName, e); + return null; + } + } + + /** + * Method to get a URL resource from the class path. + * + * @param resourceName The resource name + * @return The URL to the resource + */ + public static URL getUrlResource(final String resourceName) { + try { + final var classLoader = ResourceUtils.class.getClassLoader(); + + final String[] fileParts = SLASH_PAT.split(resourceName); + // Read the resource + var url = classLoader.getResource(resourceName); + + // Check if the resource is defined + if (url != null) { + // Return the resource as a file name + LOGGER.debug("found URL resource \"{}\" : ", url); + return url; + } else { + url = classLoader.getResource(fileParts[fileParts.length - 1]); + if (url == null) { + LOGGER.debug("cound not find URL resource \"{}\" : ", resourceName); + return null; + } + LOGGER.debug("found URL resource \"{}\" : ", url); + return url; + } + } catch (final Exception e) { + LOGGER.debug("error getting URL resource {}", resourceName, e); + return null; + } + } + + /** + * Method to get a URL resource from the local machine. + * + * @param resourceName The resource name + * @return The URL to the resource + */ + public static URL getLocalFile(final String resourceName) { + try { + // Input might already be in URL format + final var ret = new URL(resourceName); + final var f = new File(ret.toURI()); + if (f.exists()) { + return ret; + } + } catch (final Exception ignore) { + // We ignore exceptions here and catch them below + } + + try { + final var f = new File(resourceName); + // Check if the file exists + if (f.exists()) { + final var urlret = f.toURI().toURL(); + LOGGER.debug("resource \"{}\" was found on the local file system", f.toURI().toURL()); + return urlret; + } else { + LOGGER.debug("resource \"{}\" does not exist on the local file system", resourceName); + return null; + } + } catch (final Exception e) { + LOGGER.debug("error finding resource {}", resourceName, e); + return null; + } + } + + /** + * Gets the file path for a resource on the local file system or on the class path. + * + * @param resource the resource to the get the file path for + * @return the resource file path + */ + public static String getFilePath4Resource(final String resource) { + if (resource == null) { + return null; + } + + var modelFileUrl = getUrl4Resource(resource); + if (modelFileUrl != null) { + return modelFileUrl.getPath(); + } else { + return resource; + } + } + + /** + * Read the list of entries in a resource directory. + * + * @param resourceDirectoryName the name of the resource directory + * @return a set of entries + */ + public static Set getDirectoryContents(final String resourceDirectoryName) { + // Find the location of the resource, is it in a Jar or on the local file system? + var directoryUrl = ResourceUtils.getUrl4Resource(resourceDirectoryName); + + if (directoryUrl == null) { + LOGGER.debug("resource \"{}\" was not found", resourceDirectoryName); + return Collections.emptySet(); + } + + if (FILE_PROTOCOL.equals(directoryUrl.getProtocol())) { + return getDirectoryContentsLocal(directoryUrl, resourceDirectoryName); + } else if (JAR_PROTOCOL.equals(directoryUrl.getProtocol())) { + // Examine the Jar + return getDirectoryContentsJar(directoryUrl, resourceDirectoryName); + } else { + LOGGER.debug("resource \"{}\" has an unsupported protocol {}", resourceDirectoryName, + directoryUrl.getProtocol()); + return Collections.emptySet(); + } + } + + /** + * Get a list of the contents of a local resource directory. + * + * @param localResourceDirectoryUrl the local resource file URL + * @param resourceDirectoryName the name of the resource directory + * @return a set of the directory contents + */ + public static Set getDirectoryContentsLocal(final URL localResourceDirectoryUrl, + final String resourceDirectoryName) { + var localDirectory = new File(localResourceDirectoryUrl.getFile()); + + if (!localDirectory.isDirectory()) { + LOGGER.debug("resource \"{}\" is not a directory", resourceDirectoryName); + return Collections.emptySet(); + } + + Set localDirectorySet = new TreeSet<>(); + for (File localDirectoryEntry : Objects.requireNonNull(localDirectory.listFiles())) { + if (localDirectoryEntry.isDirectory()) { + localDirectorySet + .add(resourceDirectoryName + File.separator + localDirectoryEntry.getName() + File.separator); + } else { + localDirectorySet.add(resourceDirectoryName + File.separator + localDirectoryEntry.getName()); + } + } + + return localDirectorySet; + } + + /** + * Get a list of the contents of a local resource directory. + * + * @param jarResourceDirectoryUrl the name of the resource directory in the jar + * @param resourceDirectoryName the name of the resource directory + * @return a set of the directory contents + */ + public static Set getDirectoryContentsJar(final URL jarResourceDirectoryUrl, + final String resourceDirectoryName) { + String dirNameWithSlash = resourceDirectoryName + "/"; + int minLength = dirNameWithSlash.length() + 1; + var jarResourceDirectory = new File(jarResourceDirectoryUrl.getPath()); + String jarFileName = jarResourceDirectory.getParent().replaceFirst("^file:", "").replaceFirst("!.*$", ""); + + Set localDirectorySet = new TreeSet<>(); + + try (var jarFile = new JarFile(jarFileName)) { + Enumeration entries = jarFile.entries(); // NOSONAR + + while (entries.hasMoreElements()) { + /* + * Ignore sonar issue, as the entries are not being expanded here. + */ + JarEntry je = entries.nextElement(); // NOSONAR + String jeName = je.getName(); + + if (jeName.length() >= minLength && jeName.startsWith(dirNameWithSlash)) { + localDirectorySet.add(jeName); + } + } + } catch (IOException ioe) { + LOGGER.debug("error opening jar file {}", jarResourceDirectoryUrl.getPath()); + return Collections.emptySet(); + } + + return localDirectorySet; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/FeatureApiUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/FeatureApiUtils.java new file mode 100644 index 000000000..042ee9378 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/services/FeatureApiUtils.java @@ -0,0 +1,60 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * Utilities for use with "feature APIs". + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class FeatureApiUtils { + + /** + * Applies a function on each feature provider, stopping as soon as one returns true. + * + * @param providers list of feature providers + * @param predicate function to be applied to each provider + * @param handleEx function to handle any exception generated by the predicate + * (typically, by logging the message) + * @return {@code true} if one of the providers returned {@code true}, {@code false} + * otherwise + */ + public static boolean apply(List providers, Predicate predicate, + BiConsumer handleEx) { + + for (T feature : providers) { + try { + if (predicate.test(feature)) { + return true; + } + } catch (RuntimeException e) { + handleEx.accept(feature, e); + } + } + + return false; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedService.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedService.java new file mode 100644 index 000000000..c5050c835 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedService.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START======================================================= + * utils + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +/** + * This is a base interface that is used to control the order of a list + * of services (features) discovered via 'ServiceLoader'. See + * 'OrderedServiceImpl' for more details. + */ +@FunctionalInterface +public interface OrderedService { + /** + * Get sequence number. + * + * @return an integer sequence number, which determines the order of a list + * of objects implementing this interface + */ + public int getSequenceNumber(); + + + /** + * Get the name. + * + * @return the name of the ordered service + */ + public default String getName() { + return this.getClass().getName(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedServiceImpl.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedServiceImpl.java new file mode 100644 index 000000000..3726ef895 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedServiceImpl.java @@ -0,0 +1,133 @@ +/* + * ============LICENSE_START======================================================= + * utils + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is a template for building a sorted list of service instances, + * which are discovered and created using 'ServiceLoader'. + */ +public class OrderedServiceImpl { + // logger + private static final Logger logger = LoggerFactory.getLogger(OrderedServiceImpl.class); + + // sorted list of instances implementing the service + private List implementers = null; + + // 'ServiceLoader' that is used to discover and create the services + private final ServiceLoader serviceLoader; + + // use this to ensure that we only use one unique instance of each class + private static final Map, OrderedService> classToSingleton = new HashMap<>(); + + /** + * Constructor - create the 'ServiceLoader' instance. + * + * @param clazz the class object associated with 'T' (I supposed it could + * be a subclass, but I'm not sure if this is useful) + */ + public OrderedServiceImpl(Class clazz) { + // This constructor wouldn't be needed if 'T.class' was legal + serviceLoader = ServiceLoader.load(clazz); + } + + /** + * Get List of implementers. + * + * @return the sorted list of services implementing interface 'T' discovered by 'ServiceLoader'. + */ + public synchronized List getList() { + if (implementers == null) { + rebuildList(); + } + return implementers; + } + + /** + * This method is called by 'getList', but could also be called directly if + * we were running with a 'ClassLoader' that supported the dynamic addition + * of JAR files. In this case, it could be invoked in order to discover any + * new services implementing interface 'T'. This is probably a relatively + * expensive operation in terms of CPU and elapsed time, so it is best if it + * isn't invoked too frequently. + * + * @return the sorted list of services implementing interface 'T' discovered by 'ServiceLoader'. + */ + @SuppressWarnings("unchecked") + public synchronized List rebuildList() { + // build a list of all the current implementors + List tmp = new LinkedList<>(); + for (T service : serviceLoader) { + tmp.add((T) getSingleton(service)); + } + + // Sort the list according to sequence number, and then alphabetically + // according to full class name. + tmp.sort((o1, o2) -> { + int s1 = o1.getSequenceNumber(); + int s2 = o2.getSequenceNumber(); + if (s1 < s2) { + return -1; + } else if (s1 > s2) { + return 1; + } else { + return o1.getClass().getName().compareTo(o2.getClass().getName()); + } + }); + + // create an unmodifiable version of this list + implementers = Collections.unmodifiableList(tmp); + logger.info("***** OrderedServiceImpl implementers:\n {}", implementers); + return implementers; + } + + /** + * If a service implements multiple APIs managed by 'ServiceLoader', a + * separate instance is created for each API. This method ensures that + * the first instance is used in all the lists. + * + * @param service this is the object created by ServiceLoader + * @return the object to use in place of 'service'. If 'service' is the first + * object of this class created by ServiceLoader, it is returned. If not, + * the object of this class that was initially created is returned + * instead. + */ + private static synchronized OrderedService getSingleton(OrderedService service) { + // see if we already have an instance of this class + OrderedService rval = classToSingleton.get(service.getClass()); + if (rval == null) { + // No previous instance of this class exists -- use the supplied + // instance, and place it in the table. + rval = service; + classToSingleton.put(service.getClass(), service); + } + return rval; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java new file mode 100644 index 000000000..089949123 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java @@ -0,0 +1,223 @@ +/* + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.onap.policy.common.capabilities.Startable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages a series of services. The services are started in order, and stopped in reverse + * order. + */ +public class ServiceManager implements Startable { + private static final Logger logger = LoggerFactory.getLogger(ServiceManager.class); + + /** + * Manager name. + */ + @Getter + private final String name; + + /** + * Services to be started/stopped. + */ + private final Deque items = new LinkedList<>(); + + /** + * {@code True} if the services are currently running, {@code false} otherwise. + */ + private final AtomicBoolean running = new AtomicBoolean(false); + + /** + * Constructs the object, with a default name. + */ + public ServiceManager() { + this("service manager"); + } + + /** + * Constructs the object. + * + * @param name the manager's name, used for logging purposes + */ + public ServiceManager(String name) { + this.name = name; + } + + /** + * Adds a pair of service actions to the manager. + * + * @param stepName name to be logged when the service is started/stopped + * @param starter function to start the service + * @param stopper function to stop the service + * @return this manager + */ + public synchronized ServiceManager addAction(String stepName, RunnableWithEx starter, RunnableWithEx stopper) { + if (isAlive()) { + throw new IllegalStateException(name + " is already running; cannot add " + stepName); + } + + items.add(new Service(stepName, starter, stopper)); + return this; + } + + /** + * Adds a service to the manager. The manager will invoke the service's + * {@link Startable#start()} and {@link Startable#stop()} methods. + * + * @param stepName name to be logged when the service is started/stopped + * @param service object to be started/stopped + * @return this manager + */ + public synchronized ServiceManager addService(String stepName, Startable service) { + if (isAlive()) { + throw new IllegalStateException(name + " is already running; cannot add " + stepName); + } + + items.add(new Service(stepName, service::start, service::stop)); + return this; + } + + @Override + public boolean isAlive() { + return running.get(); + } + + @Override + public synchronized boolean start() { + if (isAlive()) { + throw new IllegalStateException(name + " is already running"); + } + + logger.info("{} starting", name); + + // tracks the services that have been started so far + Deque started = new LinkedList<>(); + Exception ex = null; + + for (Service item : items) { + try { + logger.info("{} starting {}", name, item.stepName); + item.starter.run(); + started.add(item); + + } catch (Exception e) { + logger.error("{} failed to start {}; rewinding steps", name, item.stepName); + ex = e; + break; + } + } + + if (ex == null) { + logger.info("{} started", name); + running.set(true); + return true; + } + + // one of the services failed to start - rewind those we've previously started + try { + rewind(started); + + } catch (ServiceManagerException e) { + logger.error("{} rewind failed", name, e); + } + + throw new ServiceManagerException(ex); + } + + @Override + public synchronized boolean stop() { + if (!isAlive()) { + throw new IllegalStateException(name + " is not running"); + } + + running.set(false); + rewind(items); + + return true; + } + + @Override + public void shutdown() { + stop(); + } + + /** + * Rewinds a list of services, stopping them in reverse order. Stops all of the + * services, even if one of the "stop" functions throws an exception. + * + * @param running services that are running, in the order they were started + * @throws ServiceManagerException if a service fails to stop + */ + private void rewind(Deque running) { + Exception ex = null; + + logger.info("{} stopping", name); + + // stop everything, in reverse order + Iterator it = running.descendingIterator(); + while (it.hasNext()) { + Service item = it.next(); + try { + logger.info("{} stopping {}", name, item.stepName); + item.stopper.run(); + } catch (Exception e) { + logger.error("{} failed to stop {}", name, item.stepName); + ex = e; + + // do NOT break or re-throw, as we must stop ALL remaining items + } + } + + logger.info("{} stopped", name); + + if (ex != null) { + throw new ServiceManagerException(ex); + } + } + + /** + * Service information. + */ + @AllArgsConstructor + private static class Service { + private String stepName; + private RunnableWithEx starter; + private RunnableWithEx stopper; + } + + /* + * Cannot use a plain Runnable, because it can't throw exceptions. Could use a + * Callable, instead, but then all the lambda expressions become rather messy, thus + * we'll stick with RunnableWithEx, and just disable the sonar warning. + */ + @FunctionalInterface + public static interface RunnableWithEx { + void run() throws Exception; // NOSONAR + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerContainer.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerContainer.java new file mode 100644 index 000000000..6afc1ab01 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerContainer.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx; + +/** + * Container, of a service manager, that implements a {@link Startable} interface by + * delegating to the contained service manager. This allows subclasses to add actions to + * the service manager, while preventing other classes from doing so. + */ +public class ServiceManagerContainer implements Startable { + + /** + * The contained manager. + */ + private final ServiceManager serviceManager; + + /** + * Constructs the object, with a default name. + */ + public ServiceManagerContainer() { + serviceManager = new ServiceManager(); + } + + /** + * Constructs the object. + * + * @param name the manager's name, used for logging purposes + */ + public ServiceManagerContainer(String name) { + serviceManager = new ServiceManager(name); + } + + public String getName() { + return serviceManager.getName(); + } + + /** + * Adds a pair of service actions to the manager. + * + * @param stepName name to be logged when the service is started/stopped + * @param starter function to start the service + * @param stopper function to stop the service + */ + protected void addAction(String stepName, RunnableWithEx starter, RunnableWithEx stopper) { + serviceManager.addAction(stepName, starter, stopper); + } + + /** + * Adds a service to the manager. The manager will invoke the service's + * {@link Startable#start()} and {@link Startable#stop()} methods. + * + * @param stepName name to be logged when the service is started/stopped + * @param service object to be started/stopped + */ + protected void addService(String stepName, Startable service) { + serviceManager.addService(stepName, service); + } + + @Override + public boolean isAlive() { + return serviceManager.isAlive(); + } + + @Override + public boolean start() { + return serviceManager.start(); + } + + @Override + public boolean stop() { + return serviceManager.stop(); + } + + @Override + public void shutdown() { + serviceManager.shutdown(); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java new file mode 100644 index 000000000..ac37b6b43 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +/** + * Exceptions thrown by the ServiceManager. + */ +public class ServiceManagerException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public ServiceManagerException() { + super(); + } + + public ServiceManagerException(String message) { + super(message); + } + + public ServiceManagerException(Throwable cause) { + super(cause); + } + + public ServiceManagerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ConstructionError.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ConstructionError.java new file mode 100644 index 000000000..d34385776 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/test/ConstructionError.java @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START==================================================== + * Common Utils-Test + * ============================================================================= + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ============================================================================= + * 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. + * ============LICENSE_END====================================================== + */ + +package org.onap.policy.common.utils.test; + +/** + * An error that occurred while trying to construct an object for a junit test. + */ +public class ConstructionError extends AssertionError { + private static final long serialVersionUID = 1L; + + /** + * Constructor. + */ + public ConstructionError() { + super(); + } + + /** + * Constructor. + * + * @param message denotes the error message + */ + public ConstructionError(final String message) { + super(message); + } + + /** + * Constructor. + * + * @param cause denotes the cause of the error + */ + public ConstructionError(final Throwable cause) { + super(cause); + } + + /** + * Constructor. + * + * @param message denotes the error message + * @param cause denotes the cause of the error + */ + public ConstructionError(final String message, final Throwable cause) { + super(message, cause); + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ErrorsTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ErrorsTester.java new file mode 100644 index 000000000..ab876b6ea --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/test/ErrorsTester.java @@ -0,0 +1,48 @@ +/* + * ============LICENSE_START==================================================== + * Common Utils-Test + * ============================================================================= + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ============================================================================= + * 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. + * ============LICENSE_END====================================================== + */ + +package org.onap.policy.common.utils.test; + +/** + * Used to test various Error subclasses. Uses reflection to identify the + * constructors that the subclass supports. + */ +public class ErrorsTester extends ThrowablesTester { + + /** + * Runs tests, on an Error subclass, for all of the standard + * constructors.If the Error subclass does not support a given + * type of constructor, then it skips that test. + * Does not throw an exception if no standard constructors + * are found. + * + * @param claz subclass to be tested + * @param this needs to be declared + * + * @return the number of constructors that were found/tested + * @throws ConstructionError + * if the Error subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public int testAllError(final Class claz) { + return testAllThrowable(claz); + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ExceptionsTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ExceptionsTester.java new file mode 100644 index 000000000..34dc5681c --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/test/ExceptionsTester.java @@ -0,0 +1,208 @@ +/* + * ============LICENSE_START==================================================== + * Common Utils-Test + * ============================================================================= + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ============================================================================= + * 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. + * ============LICENSE_END====================================================== + */ + +package org.onap.policy.common.utils.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; + +/** + * Used to test various Exception subclasses. Uses reflection to identify the + * constructors that the subclass supports. + */ +public class ExceptionsTester extends ThrowablesTester { + + /** + * Runs tests, on an Exception subclass, for all of the standard + * constructors. If the Exception subclass does not support a given + * type of constructor, then it skips that test. + * + * @param claz subclass to be tested + * @param Type of the class + * + * @return the number of constructors that were found/tested + * @throws ConstructionError + * if the Exception subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public int test(final Class claz) { + int ncons = testAllException(claz); + + assertTrue(ncons > 0); + + return ncons; + } + + /** + * Runs tests, on an Exception subclass, for all of the standard + * constructors. If the Exception subclass does not support a given + * type of constructor, then it skips that test. Does not throw + * an exception if no standard constructors are found. + * + * @param claz subclass to be tested + * @param type of the class + * + * @return the number of constructors that were found/tested + * @throws ConstructionError + * if the Exception subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public int testAllException(final Class claz) { + var ncons = 0; + + ncons += testAllThrowable(claz); + ncons += testException(claz); + ncons += testStringException(claz); + ncons += testStringExceptionBooleanBoolean(claz); + + return ncons; + } + + /** + * Tests exceptions created via the constructor that takes just an + * Exception. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the cause's message
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
+ * + *

If the Exception subclass does not support this type of constructor, + * then this method simply returns. + * + * @param claz subclass to be tested + * @param Type of the class + * + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Exception subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public int testException(final Class claz) { + Constructor cons = getConstructor(claz, "exception", + Exception.class); + + if (cons == null) { + return 0; + } + + var cause = new Exception(EXPECTED_EXCEPTION_MSG); + var ex = newInstance(cons, cause); + + assertNotNull(ex.toString()); + assertEquals(ex.getMessage(), ex.getMessage()); + assertEquals(cause, ex.getCause()); + + return 1; + } + + /** + * Tests exceptions created via the constructor that takes a String + * and an Exception. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to + * the constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
+ * + *

If the Exception subclass does not support this type of + * constructor, then this method simply returns. + * + * @param claz subclass to be tested + * @param Type of the class + * + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Exception subclass cannot be constructed + */ + public int testStringException( + final Class claz) { + Constructor cons = getConstructor(claz, "string-exception", + String.class, Exception.class); + if (cons == null) { + return 0; + } + + var cause = new Exception(EXPECTED_EXCEPTION_MSG); + var ex = newInstance(cons, "world", cause); + + assertNotNull(ex.toString()); + assertEquals("world", ex.getMessage()); + assertEquals(cause, ex.getCause()); + + return 1; + } + + /** + * Tests exceptions created via the constructor that takes a String, an + * Exception, and two booleans. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
  • suppressed exceptions can be added, if enabled
  • + *
  • the stack trace can be added, if enabled
  • + *
+ * + *

If the Exception subclass does not support this type of constructor, + * then this method simply returns. + * + * @param claz subclass to be tested + * @param Type of the class + * + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Exception subclass cannot be constructed + */ + public int testStringExceptionBooleanBoolean( + final Class claz) { + + Constructor cons = getConstructor(claz, + "string-exception-flags", String.class, Exception.class, + Boolean.TYPE, Boolean.TYPE); + + if (cons == null) { + return 0; + } + + // test each combination of "message" and "cause" + testMessageCauseCombos(cons); + + // test each combination of the boolean flags + testFlagCombos(cons); + + return 1; + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ThrowablesTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ThrowablesTester.java new file mode 100644 index 000000000..62584c768 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/test/ThrowablesTester.java @@ -0,0 +1,564 @@ +/* + * ============LICENSE_START==================================================== + * Common Utils-Test + * ============================================================================= + * Copyright (C) 2018, 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ============================================================================= + * 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. + * ============LICENSE_END====================================================== + */ + +package org.onap.policy.common.utils.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used to test various Throwable subclasses. Uses reflection to identify the + * constructors that the subclass supports. + */ +public class ThrowablesTester { + + private static Logger logger = + LoggerFactory.getLogger(ThrowablesTester.class); + + public static final String EXPECTED_EXCEPTION_MSG = + "expected exception"; + private static final String EXPECTED_SUPPRESSED_EXCEPTION_MSG = + "expected suppressed exception"; + + /** + * Passed as a "cause" to constructors. + */ + public static final Exception CAUSE = + new Exception(EXPECTED_EXCEPTION_MSG); + + /** + * Passed to new objects via the addSuppressed() method.. + */ + public static final Throwable SUPPRESSED = + new Throwable(EXPECTED_SUPPRESSED_EXCEPTION_MSG); + + /** + * Runs tests, on an Throwable subclass, for all of the + * standard constructors. If the Throwable subclass does + * not support a given type of constructor, then it skips + * that test. Does not throw an exception if no + * standard constructors are found. + * + * @param claz subclass to be tested + * @param To be defined + * @return the number of constructors that were found/tested + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final int testAllThrowable( + final Class claz) { + var ncons = 0; + + ncons += testDefault(claz); + ncons += testString(claz); + ncons += testThrowable(claz); + ncons += testStringThrowable(claz); + ncons += testStringThrowableBooleanBoolean(claz); + + return ncons; + } + + /** + * Tests Throwable objects created via the default constructor. Verifies + * that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns null
  • + *
  • getCause() returns null
  • + *
+ * + *

If the Throwable subclass does not support this type of + * constructor, then this method simply returns. + * + * @param claz subclass to be tested + * @param to be defined + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final int testDefault( + final Class claz) { + Constructor cons = getConstructor(claz, "default"); + if (cons == null) { + return 0; + } + + var ex = newInstance(cons); + + assertNotNull(ex.toString()); + assertNull(ex.getMessage()); + assertNull(ex.getCause()); + + return 1; + } + + /** + * Tests Throwable objects created via the constructor that takes just a + * String. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns null
  • + *
+ * + *

If the Throwable subclass does not support this type of constructor, + * then this method simply returns. + * + * @param claz + * subclass to be tested + * @param to be defined + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final int testString(final Class claz) { + Constructor cons = getConstructor(claz, "string", + String.class); + if (cons == null) { + return 0; + } + + var ex = newInstance(cons, "hello"); + + assertNotNull(ex.toString()); + assertEquals("hello", ex.getMessage()); + assertNull(ex.getCause()); + + return 1; + } + + /** + * Tests Throwable objects created via the constructor that takes just a + * Throwable. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the cause's message
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
+ * + *

If the Throwable subclass does not support this type of constructor, + * then this method simply returns. + * + * @param claz + * subclass to be tested + * @param to be defined + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final int testThrowable( + final Class claz) { + Constructor cons = getConstructor(claz, "throwable", + Throwable.class); + if (cons == null) { + return 0; + } + + var ex = newInstance(cons, CAUSE); + + assertEquals(ex.getMessage(), ex.getMessage()); + assertNotNull(ex.toString()); + assertEquals(CAUSE, ex.getCause()); + + return 1; + } + + /** + * Tests Throwable objects created via the constructor that takes + * a String and a Throwable. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
+ * + *

If the Throwable subclass does not support this type of constructor, + * then this method simply returns. + * + * @param claz subclass to be tested + * @param to be defined + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final int testStringThrowable( + final Class claz) { + Constructor cons = getConstructor(claz, "string-throwable", + String.class, Throwable.class); + if (cons == null) { + return 0; + } + + var ex = newInstance(cons, "world", CAUSE); + + assertNotNull(ex.toString()); + assertEquals("world", ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + + return 1; + } + + /** + * Tests Throwable objects created via the constructor that takes + * a String, a Throwable, and two booleans. Verifies that: + *

    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
  • suppressed exceptions can be added, if enabled
  • + *
  • the stack trace can be added, if enabled
  • + *
+ * + *

If the Throwable subclass does not support this type of constructor, + * then this method simply returns. + * + * @param claz + * subclass to be tested + * @param to be defined + * @return {@code 1}, if the subclass supports this type of constructor, + * {@code 0} otherwise + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final int + testStringThrowableBooleanBoolean( + final Class claz) { + Constructor cons = getConstructor(claz, + "string-throwable-flags", + String.class, Throwable.class, + Boolean.TYPE, Boolean.TYPE); + if (cons == null) { + return 0; + } + + // test each combination of "message" and "cause" + testMessageCauseCombos(cons); + + // test each combination of the boolean flags + testSuppressStack(cons); + testSuppressNoStack(cons); + testNoSuppressStack(cons); + testNoSuppressNoStack(cons); + + return 1; + } + + /** + * Tests each combination of values for the "message" and the "cause" + * when using the constructor that takes a String, + * a Throwable/Exception, and two booleans. Verifies that expected + * values are returned + *

    + * toString(), + * getMessage(), and getCause(). + *
+ * + * @param cons + * constructor to be invoked + * @param to be defined + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final void testMessageCauseCombos( + final Constructor cons) { + T ex; + + ex = newInstance(cons, null, null, true, true); + assertNotNull(ex.toString()); + assertNull(ex.getMessage()); + assertNull(ex.getCause()); + + ex = newInstance(cons, "abc", null, true, true); + assertNotNull(ex.toString()); + assertEquals("abc", ex.getMessage()); + assertNull(ex.getCause()); + + ex = newInstance(cons, null, CAUSE, true, true); + assertNotNull(ex.toString()); + assertNull(ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + + ex = newInstance(cons, "xyz", CAUSE, true, true); + assertNotNull(ex.toString()); + assertEquals("xyz", ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + } + + /** + * Tests each combination of values for the "message" and the + * "cause" when using the constructor that takes a String, + * a Throwable/Exception, and two booleans. Verifies that + * expected values are returned by + *
    + * toString(), + * getMessage(), and getCause(). + *
+ * + * @param cons + * constructor to be invoked + * @param to be defined + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final void testFlagCombos( + final Constructor cons) { + testSuppressStack(cons); + testSuppressNoStack(cons); + testNoSuppressStack(cons); + testNoSuppressNoStack(cons); + } + + /** + * Tests Throwable objects constructed with + * {@code enableSuppression=true} and + * {@code writableStackTrace=true}. Verifies that: + *
    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
  • suppressed exceptions are added
  • + *
  • the stack trace is added
  • + *
+ * + * @param cons + * the throwable's class constructor + * @param to be defined + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final void testSuppressStack( + final Constructor cons) { + var ex = newInstance(cons, "yes,yes", CAUSE, true, true); + + ex.addSuppressed(SUPPRESSED); + + assertNotNull(ex.toString()); + assertEquals("yes,yes", ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + + assertEquals(1, ex.getSuppressed().length); + assertEquals(SUPPRESSED, ex.getSuppressed()[0]); + + assertTrue(ex.getStackTrace().length > 0); + } + + /** + * Tests Throwable objects constructed with + * {@code enableSuppression=true} and + * {@code writableStackTrace=false}. Verifies that: + *
    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
  • suppressed exceptions are added
  • + *
  • the stack trace is not added
  • + *
+ * + * @param cons + * the throwable's class constructor + * @param to be defined + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final void testSuppressNoStack( + final Constructor cons) { + var ex = newInstance(cons, "yes,no", CAUSE, true, false); + + ex.addSuppressed(SUPPRESSED); + + assertNotNull(ex.toString()); + assertEquals("yes,no", ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + + assertEquals(1, ex.getSuppressed().length); + assertEquals(SUPPRESSED, ex.getSuppressed()[0]); + + assertEquals(0, ex.getStackTrace().length); + } + + /** + * Tests Throwable objects constructed with + * {@code enableSuppression=false} and + * {@code writableStackTrace=true}. Verifies that: + *
    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
  • suppressed exceptions are not added
  • + *
  • the stack trace is added
  • + *
+ * + * @param cons + * the throwable's class constructor + * @param to be defined + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final void testNoSuppressStack( + final Constructor cons) { + var ex = newInstance(cons, "no,yes", CAUSE, false, true); + + ex.addSuppressed(SUPPRESSED); + + assertNotNull(ex.toString()); + assertEquals("no,yes", ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + + assertEquals(0, ex.getSuppressed().length); + + assertTrue(ex.getStackTrace().length > 0); + } + + /** + * Tests Throwable objects constructed with + * {@code enableSuppression=false} and + * {@code writableStackTrace=false}. Verifies that: + *
    + *
  • toString() returns a non-null value
  • + *
  • getMessage() returns the original message passed to the + * constructor
  • + *
  • getCause() returns the original cause passed to the + * constructor
  • + *
  • suppressed exceptions are not added
  • + *
  • the stack trace is not added
  • + *
+ * @param cons + * the throwable's class constructor + * @param to be defined + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + * @throws AssertionError + * if the constructed objects fail to pass various tests + */ + public final void testNoSuppressNoStack( + final Constructor cons) { + var ex = newInstance(cons, "no,no", CAUSE, false, false); + + ex.addSuppressed(SUPPRESSED); + + assertNotNull(ex.toString()); + assertEquals("no,no", ex.getMessage()); + assertEquals(CAUSE, ex.getCause()); + + assertEquals(0, ex.getSuppressed().length); + assertEquals(0, ex.getStackTrace().length); + } + + /** + * Attempts to get a constructor for objects of a given type. + * + * @param claz + * class of objects whose constructor is to be gotten + * @param to be defined + * @param testType + * type of test being run + * @param argTypes + * argument types to be passed to the constructor + * @return the desired constructor, or {@code null} if the desired + * constructor is not available + */ + protected Constructor getConstructor( + final Class claz, + final String testType, + final Class... argTypes) { + + try { + return claz.getConstructor(argTypes); + + } catch (NoSuchMethodException | SecurityException e) { + // this constructor is not defined so nothing to test + logger.debug("skipped test, no constructor for: {}", claz, e); + return null; + } + } + + /** + * Creates a new instance of an Throwable subclass. + * + * @param cons + * subclass constructor + * @param to be defined + * @param args + * arguments to be passed to the constructor + * @return a new instance of the Throwable subclass + * @throws ConstructionError + * if the Throwable subclass cannot be constructed + */ + protected T newInstance( + final Constructor cons, + final Object... args) { + try { + return cons.newInstance(args); + + } catch (InstantiationException | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + + throw new ConstructionError(e); + } + + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ToStringTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ToStringTester.java new file mode 100644 index 000000000..ab09291c2 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/test/ToStringTester.java @@ -0,0 +1,63 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.test; + +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.anything; + +import com.openpojo.reflection.PojoClass; +import com.openpojo.validation.affirm.Affirm; +import com.openpojo.validation.test.Tester; +import com.openpojo.validation.utils.ValidationHelper; +import lombok.AllArgsConstructor; +import org.hamcrest.Matcher; + + +/** + * Class to provide toString testing utility for testing pojo classes. + * + * @author Ram Krishna Verma (ram.krishna.verma@est.tech) + */ +@AllArgsConstructor +public class ToStringTester implements Tester { + + private final Matcher matcher; + + public ToStringTester() { + matcher = anything(); + } + + @SuppressWarnings("unchecked") + @Override + public void run(final PojoClass pojoClass) { + final Class clazz = pojoClass.getClazz(); + if (anyOf(matcher).matches(clazz)) { + final Object classInstance = ValidationHelper.getBasicInstance(pojoClass); + + Affirm.affirmFalse("Found default toString output", + classInstance.toString().matches(Object.class.getName() + "@" + "\\w+")); + } + + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/log/logback/ExtractAppender.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/log/logback/ExtractAppender.java new file mode 100644 index 000000000..27d9fcc65 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/test/log/logback/ExtractAppender.java @@ -0,0 +1,191 @@ +/* + * ============LICENSE_START==================================================== + * Common Utils-Test + * ============================================================================= + * Copyright (C) 2018-2019, 2021 AT&T Intellectual Property. All rights reserved. + * ============================================================================= + * 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. + * ============LICENSE_END====================================================== + */ + +package org.onap.policy.common.utils.test.log.logback; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.google.re2j.Matcher; +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** + * This is an appender that is intended for use by JUnit tests that wish to + * capture logged messages. The appender takes an optional list of regular + * expressions that are used to identify and extract data of interest. + *

+ * If no patterns are provided, then every logged message is recorded. However, + * if patterns are provided, then only messages that match one of the patterns + * are recorded. In addition, if the pattern contains a capture group that is + * non-null, only the captured group is recorded. Otherwise, the entire portion + * of the message that matches the pattern is recorded. + *

+ * All operations are thread-safe. + */ +public class ExtractAppender extends AppenderBase { + + /** + * Extracted text is placed here. + */ + private final Queue extracted; + + /** + * Regular expressions/Patterns to be used to extract text. Uses a + * LinkedHashMap so that order is preserved. + */ + private final LinkedHashMap patterns; + + /** + * Records every message that is logged. + */ + public ExtractAppender() { + this(new LinkedList<>()); + } + + /** + * Records portions of messages that match one of the regular + * expressions. + * + * @param regex + * regular expression (i.e., {@link Pattern}) to match + */ + public ExtractAppender(final String... regex) { + this(new LinkedList<>(), regex); + } + + /** + * Rather than allocating an internal queue to store matched messages, + * messages are recorded in the specified target queue using the + * {@link Queue#offer(Object)} method. Note: whenever the queue is used, + * it will be synchronized to prevent simultaneous accesses. + * + * @param target - queue into which the matched text should be placed + * @param regex regular expression (i.e., {@link Pattern}) to match + */ + public ExtractAppender(final Queue target, + final String... regex) { + extracted = target; + patterns = new LinkedHashMap<>(regex.length); + + for (String re : regex) { + patterns.put(re, Pattern.compile(re)); + } + } + + /* + * (non-Javadoc) + * + * @see ch.qos.logback.core.AppenderBase#append(Object) + */ + @Override + protected void append(final ILoggingEvent event) { + + String msg = event.getFormattedMessage(); + + synchronized (patterns) { + if (patterns.isEmpty()) { + addExtraction(msg); + return; + } + + for (Pattern p : patterns.values()) { + var matcher = p.matcher(msg); + + if (matcher.find()) { + addGroupMatch(matcher); + break; + } + } + } + } + + /** + * Adds the first match group to {@link #extracted}. + * + * @param mat the matcher containing the groups + * + */ + private void addGroupMatch(final Matcher mat) { + int ngroups = mat.groupCount(); + + for (var x = 1; x <= ngroups; ++x) { + String txt = mat.group(x); + + if (txt != null) { + addExtraction(txt); + return; + } + } + + addExtraction(mat.group()); + } + + /** + * Adds an item to {@link #extracted}, in a thread-safe manner. + * It uses the queue's offer() method so that the queue + * can discard the item if it so chooses, without generating + * an exception. + * + * @param txt + * text to be added + */ + private void addExtraction(final String txt) { + synchronized (extracted) { + extracted.offer(txt); + } + } + + /** + * Gets the text that has been extracted. + * + * @return a copy of the text that has been extracted + */ + public List getExtracted() { + synchronized (extracted) { + return new ArrayList<>(extracted); + } + } + + /** + * Clears the list of extracted text. + */ + public void clearExtractions() { + synchronized (extracted) { + extracted.clear(); + } + } + + /** + * Adds a pattern to be matched by this appender. + * + * @param regex + * regular expression (i.e., {@link Pattern}) to match + */ + public void setPattern(final String regex) { + synchronized (patterns) { + patterns.put(regex, Pattern.compile(regex)); + } + } + +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/validation/Assertions.java b/policy-common/src/main/java/org/onap/policy/common/utils/validation/Assertions.java new file mode 100644 index 000000000..8e4742049 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/validation/Assertions.java @@ -0,0 +1,188 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2016-2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2019 Nordix Foundation. + * Modifications Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.validation; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Class Assertions is a static class that is used as a shorthand for assertions in the source code. + * It throws runtime exceptions on assertion fails. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class Assertions { + // Logger for this class + private static final Logger LOGGER = LoggerFactory.getLogger(Assertions.class); + + /** + * Gets the validation message for a string parameter. + * + * @param parameterName the string parameter name + * @param parameterValue the string parameter value + * @param pattern The regular expression + * @return null if the parameter is valid, the validation message otherwise + */ + public static String getStringParameterValidationMessage(final String parameterName, final String parameterValue, + final String pattern) { + try { + validateStringParameter(parameterName, parameterValue, pattern); + } catch (IllegalArgumentException e) { + String message = "parameter " + parameterName + " with value " + parameterValue + + " does not match regular expression " + pattern; + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(message, e); + } + + return message; + } + + return null; + } + + /** + * Checks if a string parameter matches a regular expression. + * + * @param parameterName the string parameter name + * @param parameterValue the string parameter value + * @param pattern The regular expression + * @return the trimmed string + */ + public static String validateStringParameter(final String parameterName, final String parameterValue, + final String pattern) { + argumentNotNull(parameterName, "parameter name is null"); + argumentNotNull(parameterValue, "parameter \"" + parameterName + "\" is null"); + argumentNotNull(pattern, "parameter pattern is null"); + + final String trimmedValue = parameterValue.trim(); + if (trimmedValue.matches(pattern)) { + return trimmedValue; + } else { + throw new IllegalArgumentException("parameter \"" + parameterName + "\": value \"" + parameterValue + + "\", does not match regular expression \"" + pattern + "\""); + } + } + + /** + * Used as a shorthand to check that method arguments are not null, throws IllegalArgumentException on error. + * + * @param the generic type of the argument to check + * @param value the value of the type + * @param message the error message to issue + */ + public static void argumentNotNull(final T value, final String message) { + if (value == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Used as a shorthand to check that method arguments are not false, throws IllegalArgumentException on error. + * + * @param value the value to check if false + * @param message the error message to issue + */ + public static void argumentNotFalse(final boolean value, final String message) { + if (!value) { + throw new IllegalArgumentException(message); + } + } + + /** + * Used as a shorthand to check that method arguments are not null, throws an exception of the specified type on + * error. + * + * @param the generic type of the argument to check + * @param the exception to throw if incoming value is null + * @param value the value of the type + * @param exceptionClass the class of exception to return an instance of + * @param message the error message to issue + * @throws E an instance of the passed Exception Class + */ + public static void argumentOfClassNotNull(final T value, final Class exceptionClass, + final String message) throws E { + if (value == null) { + // Instantiate the exception and throw it + try { + throw exceptionClass.getConstructor(String.class).newInstance(message); + } catch (final Exception errorException) { + throw new IllegalArgumentException(message, errorException); + } + } + } + + /** + * Used as a shorthand to check that method argument is not false, throws an exception of the specified type on + * error. + * + * @param the exception to throw if incoming value is false + * @param value the value to check if false + * @param exceptionClass the class of exception to return an instance of + * @param message the error message to issue + * @throws E an instance of the passed Exception Class + */ + public static void argumentOfClassNotFalse(final boolean value, final Class exceptionClass, + final String message) throws E { + if (!value) { + // Instantiate the exception and throw it + try { + throw exceptionClass.getConstructor(String.class).newInstance(message); + } catch (final Exception errorException) { + throw new IllegalArgumentException(message, errorException); + } + } + } + + /** + * Used as a shorthand to check that an object is an instance of a given class, throws IllegalArgumentException on + * error. + * + * @param the generic type of the argument to check + * @param objectInstance the object instance for which to check the class + * @param requiredClass the class that the object should be an instance of + * @throws IllegalArgumentException if the incoming object is not an instance of requiredClass + */ + public static void instanceOf(final Object objectInstance, final Class requiredClass) { + if (!requiredClass.isAssignableFrom(objectInstance.getClass())) { + throw new IllegalArgumentException(objectInstance.getClass().getName() + " is not an instance of " + + requiredClass.getName()); + } + } + + /** + * Used as a shorthand to check that an instance of a class can be an instance of a given class, throws + * IllegalArgumentException on error. + * + * @param the generic type of the argument to check + * @param checkClass the class to check + * @param requiredClass the class that the object should be an instance of + * @throws IllegalArgumentException if the incoming object is not an instance of requiredClass + */ + public static void assignableFrom(final Class checkClass, final Class requiredClass) { + if (!requiredClass.isAssignableFrom(checkClass)) { + throw new IllegalArgumentException(checkClass.getName() + " is not an instance of " + + requiredClass.getName()); + } + } +} diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/validation/Version.java b/policy-common/src/main/java/org/onap/policy/common/utils/validation/Version.java new file mode 100644 index 000000000..46e006bd3 --- /dev/null +++ b/policy-common/src/main/java/org/onap/policy/common/utils/validation/Version.java @@ -0,0 +1,129 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP COMMON + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.validation; + +import com.google.re2j.Pattern; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Version of an object within the model. Versions are assumed to be of the form: major or major.minor.patch, where each + * component is numeric. + */ +@Data +@RequiredArgsConstructor +@NoArgsConstructor(force = true) +public class Version implements Comparable { + private static final Logger logger = LoggerFactory.getLogger(Version.class); + + /** + * Pattern to match a version of the form, major or major.minor.patch, where all components are numeric. + */ + private static final Pattern VERSION_PAT = Pattern.compile("(\\d+)([.](\\d+)[.](\\d+))?"); + + private final int major; + private final int minor; + private final int patch; + + + /** + * String constructor. + * + * @param versionString the version string + */ + public Version(@NonNull final String versionString) { + var newVersion = makeVersion("String", "constructor", versionString); + + if (newVersion != null) { + this.major = newVersion.major; + this.minor = newVersion.minor; + this.patch = newVersion.patch; + } else { + this.major = 0; + this.minor = 0; + this.patch = 0; + } + } + + /** + * Creates a version object. + * + * @param type type of object with which the version is associated, used when logging + * @param name name with which the version is associated, used when logging + * @param versionText the version, in textual form + * @return a new version, or {@code null} if the version cannot be created from the key (e.g., the key has a version + * that does not match the major.minor.patch form) + */ + public static Version makeVersion(String type, String name, String versionText) { + var matcher = VERSION_PAT.matcher(versionText); + if (!matcher.matches()) { + logger.info("invalid version for {} {}: {}", type, name, versionText); + return null; + } + + try { + if (matcher.group(2) == null) { + // form: major + return new Version(Integer.parseInt(matcher.group(1)), 0, 0); + + } else { + // form: major.minor.patch + return new Version(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(3)), + Integer.parseInt(matcher.group(4))); + } + + } catch (NumberFormatException e) { + logger.info("invalid version for {} {}: {}", type, name, versionText, e); + return null; + } + } + + /** + * Generates a new version from a string. + * + * @return a new version, of the form major.0.0, where "major" is one more than "this" version's major number + */ + public Version newVersion() { + return new Version(major + 1, 0, 0); + } + + @Override + public int compareTo(Version other) { + var result = Integer.compare(major, other.major); + if (result != 0) { + return result; + } + if ((result = Integer.compare(minor, other.minor)) != 0) { + return result; + } + return Integer.compare(patch, other.patch); + } + + @Override + public String toString() { + return major + "." + minor + "." + patch; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/JsonListenerTest.java b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/JsonListenerTest.java new file mode 100644 index 000000000..c2e5f90d5 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/JsonListenerTest.java @@ -0,0 +1,123 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.endpoints.listeners; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.common.utils.test.log.logback.ExtractAppender; +import org.slf4j.LoggerFactory; + +class JsonListenerTest { + + /** + * Used to attach an appender to the class' logger. + */ + private static final Logger logger = (Logger) LoggerFactory.getLogger(JsonListener.class); + private static final ExtractAppender appender = new ExtractAppender(); + + /** + * Original logging level for the logger. + */ + private static Level saveLevel; + + private static final CommInfrastructure INFRA = CommInfrastructure.NOOP; + private static final String TOPIC = "my-topic"; + private static final String JSON = "{'abc':'def'}".replace('\'', '"'); + + private JsonListener primary; + + /** + * Initializes statics. + */ + @BeforeAll + public static void setUpBeforeClass() { + saveLevel = logger.getLevel(); + logger.setLevel(Level.INFO); + + appender.setContext(logger.getLoggerContext()); + appender.start(); + } + + @AfterAll + public static void tearDownAfterClass() { + logger.setLevel(saveLevel); + appender.stop(); + } + + /** + * Initializes mocks and a listener. + */ + @BeforeEach + public void setUp() { + appender.clearExtractions(); + + primary = new JsonListener() { + @Override + public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco) { + // do nothing + } + }; + } + + @AfterEach + public void tearDown() { + logger.detachAppender(appender); + } + + @Test + void testOnTopicEvent() { + logger.addAppender(appender); + + primary = spy(primary); + + // success + primary.onTopicEvent(INFRA, TOPIC, JSON); + verify(primary).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + + // repeat + primary.onTopicEvent(INFRA, TOPIC, JSON); + verify(primary, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + + assertFalse(appender.getExtracted().toString().contains("unable to decode")); + + // invalid json - decode fails + appender.clearExtractions(); + primary.onTopicEvent(INFRA, TOPIC, "["); + assertTrue(appender.getExtracted().toString().contains("unable to decode")); + verify(primary, times(2)).onTopicEvent(any(), any(), any(StandardCoderObject.class)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcherTest.java b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcherTest.java new file mode 100644 index 000000000..e0184bd12 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcherTest.java @@ -0,0 +1,179 @@ +/*-- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.endpoints.listeners; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.common.utils.test.log.logback.ExtractAppender; +import org.slf4j.LoggerFactory; + +class MessageTypeDispatcherTest { + + /** + * Used to attach an appender to the class' logger. + */ + private static final Logger logger = (Logger) LoggerFactory.getLogger(MessageTypeDispatcher.class); + private static final ExtractAppender appender = new ExtractAppender(); + + /** + * Original logging level for the logger. + */ + private static Level saveLevel; + + private static final CommInfrastructure INFRA = CommInfrastructure.NOOP; + private static final String TYPE_FIELD = "msg-type"; + private static final String TOPIC = "my-topic"; + private static final String TYPE1 = "msg-type-1"; + private static final String TYPE2 = "msg-type-2"; + + private MessageTypeDispatcher primary; + + private ScoListener secondary1; + private ScoListener secondary2; + + /** + * Initializes statics. + */ + @BeforeAll + public static void setUpBeforeClass() { + saveLevel = logger.getLevel(); + logger.setLevel(Level.INFO); + + appender.setContext(logger.getLoggerContext()); + appender.start(); + } + + @AfterAll + public static void tearDownAfterClass() { + logger.setLevel(saveLevel); + appender.stop(); + } + + /** + * Initializes mocks and a listener. + */ + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + appender.clearExtractions(); + + secondary1 = mock(ScoListener.class); + secondary2 = mock(ScoListener.class); + + primary = new MessageTypeDispatcher(TYPE_FIELD); + } + + @AfterEach + public void tearDown() { + logger.detachAppender(appender); + } + + @Test + void testRegister_testUnregister() { + primary.register(TYPE1, secondary1); + primary.register(TYPE2, secondary2); + + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1)); + verify(secondary1).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + verify(secondary2, never()).onTopicEvent(any(), any(), any()); + + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1)); + verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + verify(secondary2, never()).onTopicEvent(any(), any(), any()); + + primary.unregister(TYPE1); + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1)); + verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + verify(secondary2, never()).onTopicEvent(any(), any(), any()); + + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE2)); + verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + verify(secondary2).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + + // unregister again + primary.unregister(TYPE1); + + // unregister second type + primary.unregister(TYPE2); + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1)); + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE2)); + verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + verify(secondary2, times(1)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + } + + @Test + void testOnTopicEvent() { + primary.register(TYPE1, secondary1); + + logger.addAppender(appender); + + // success + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1)); + verify(secondary1).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + + // repeat + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1)); + verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class)); + + assertFalse(appender.getExtracted().toString().contains("unable to extract")); + assertFalse(appender.getExtracted().toString().contains("discarding event of type")); + + // no message type + appender.clearExtractions(); + primary.onTopicEvent(INFRA, TOPIC, "{}"); + assertTrue(appender.getExtracted().toString().contains("unable to extract")); + verify(secondary1, times(2)).onTopicEvent(any(), any(), any()); + + // unknown type + appender.clearExtractions(); + primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE2)); + assertTrue(appender.getExtracted().toString().contains("discarding event of type")); + verify(secondary1, times(2)).onTopicEvent(any(), any(), any()); + } + + /** + * Makes a JSON message of the given type. + * + * @param msgType the message type + * @return a JSON message of the given type + */ + private String makeMessage(String msgType) { + String json = "{'" + TYPE_FIELD + "':'" + msgType + "', 'abc':'def'}"; + return json.replace('\'', '"'); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/ScoListenerTest.java b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/ScoListenerTest.java new file mode 100644 index 000000000..cb8fb70d6 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/ScoListenerTest.java @@ -0,0 +1,190 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.endpoints.listeners; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.common.utils.test.log.logback.ExtractAppender; +import org.slf4j.LoggerFactory; + +class ScoListenerTest { + + /** + * Used to attach an appender to the class' logger. + */ + private static final Logger logger = (Logger) LoggerFactory.getLogger(ScoListener.class); + private static final ExtractAppender appender = new ExtractAppender(); + + /** + * Original logging level for the logger. + */ + private static Level saveLevel; + + private static final CommInfrastructure INFRA = CommInfrastructure.NOOP; + private static final String TOPIC = "my-topic"; + private static final String NAME = "pdp_1"; + + private static final Coder coder = new StandardCoder(); + + private ScoListener primary; + + /** + * Initializes statics. + */ + @BeforeAll + public static void setUpBeforeClass() { + saveLevel = logger.getLevel(); + logger.setLevel(Level.INFO); + + appender.setContext(logger.getLoggerContext()); + appender.start(); + } + + @AfterAll + public static void tearDownAfterClass() { + logger.setLevel(saveLevel); + appender.stop(); + } + + /** + * Create various mocks and primary handler. + */ + @BeforeEach + public void setUp() { + appender.clearExtractions(); + + primary = new ScoListener(MyMessage.class) { + @Override + public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco, + MyMessage message) { + // do nothing + } + }; + } + + @AfterEach + public void tearDown() { + logger.detachAppender(appender); + } + + @Test + void testOnTopicEvent() { + primary = spy(primary); + + MyMessage status = new MyMessage(NAME); + StandardCoderObject sco = makeSco(status); + primary.onTopicEvent(INFRA, TOPIC, sco); + verify(primary).onTopicEvent(INFRA, TOPIC, sco, status); + + assertFalse(appender.getExtracted().toString().contains("unable to decode")); + + // undecodable message + logger.addAppender(appender); + primary.onTopicEvent(INFRA, TOPIC, makeSco()); + verify(primary, times(1)).onTopicEvent(INFRA, TOPIC, sco, status); + assertTrue(appender.getExtracted().toString().contains("unable to decode")); + } + + /** + * Makes a standard object from a JSON string. + * + * @return a standard object representing the message + */ + private StandardCoderObject makeSco() { + try { + return coder.decode("[]", StandardCoderObject.class); + + } catch (CoderException e) { + throw new RuntimeException(e); + } + } + + /** + * Makes a standard object from a status message. + * + * @param source message to be converted + * @return a standard object representing the message + */ + private StandardCoderObject makeSco(MyMessage source) { + try { + return coder.toStandard(source); + + } catch (CoderException e) { + throw new RuntimeException(e); + } + } + + protected static class MyMessage { + private String name; + + public MyMessage() { + super(); + } + + public MyMessage(String name) { + this.name = name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MyMessage other = (MyMessage) obj; + if (name == null) { + return other.name == null; + } else { + return name.equals(other.name); + } + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java new file mode 100644 index 000000000..72cb41db7 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java @@ -0,0 +1,104 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class DoubleConverterTest { + + @Test + void testConvertFromDoubleObject() { + // these should be unchanged + assertNull(DoubleConverter.convertFromDouble((Object) null)); + assertEquals("hello", DoubleConverter.convertFromDouble("hello")); + assertEquals("10.0", DoubleConverter.convertFromDouble("10.0")); + assertEquals(12.5, DoubleConverter.convertFromDouble(12.5)); + assertEquals(12, DoubleConverter.convertFromDouble(12)); + assertEquals(12L, DoubleConverter.convertFromDouble(12L)); + + // positive and negative int + assertEquals(10, DoubleConverter.convertFromDouble(10.0)); + assertEquals(-10, DoubleConverter.convertFromDouble(-10.0)); + + // positive and negative long + assertEquals(100000000000L, DoubleConverter.convertFromDouble(100000000000.0)); + assertEquals(-100000000000L, DoubleConverter.convertFromDouble(-100000000000.0)); + + // list + List list = new ArrayList<>(); + list.add("list"); + list.add(null); + list.add(21.0); + list = (List) DoubleConverter.convertFromDouble((Object) list); + assertEquals("[list, null, 21]", list.toString()); + + // map + Map map = new LinkedHashMap<>(); + map.put("map-A", "map-value"); + map.put("map-B", null); + map.put("map-C", 22.0); + map = (Map) DoubleConverter.convertFromDouble((Object) map); + assertEquals("{map-A=map-value, map-B=null, map-C=22}", map.toString()); + } + + @Test + void testConvertFromDoubleList() { + // null is ok + DoubleConverter.convertFromDouble((List) null); + + List list = new ArrayList<>(); + list.add("world"); + list.add(20.0); + + List nested = new ArrayList<>(); + list.add(nested); + nested.add(30.0); + + DoubleConverter.convertFromDouble(list); + + assertEquals("[world, 20, [30]]", list.toString()); + } + + @Test + void testConvertFromDoubleMap() { + // null is ok + DoubleConverter.convertFromDouble((Map) null); + + Map map = new LinkedHashMap<>(); + map.put("keyA", "valueA"); + map.put("keyB", 200.0); + + Map nested = new LinkedHashMap<>(); + map.put("keyC", nested); + nested.put("nested-key", 201.0); + + DoubleConverter.convertFromDouble(map); + assertEquals("{keyA=valueA, keyB=200, keyC={nested-key=201}}", map.toString()); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java new file mode 100644 index 000000000..579b9fda2 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java @@ -0,0 +1,229 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.ws.rs.core.MediaType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import lombok.ToString; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GsonMessageBodyHandlerTest { + private static final String GEN_TYPE = "some-type"; + private static final String[] subtypes = {"json", "jSoN", "hello+json", "javascript", "x-javascript", "x-json"}; + + @SuppressWarnings("rawtypes") + private static final Class GEN_CLASS = MyObject.class; + + @SuppressWarnings("unchecked") + private static final Class CLASS_OBJ = GEN_CLASS; + + private GsonMessageBodyHandler hdlr; + + @BeforeEach + void setUp() { + hdlr = new GsonMessageBodyHandler(); + } + + @Test + void testIsWriteable() { + // null media type + assertTrue(hdlr.isWriteable(null, null, null, null)); + + for (String subtype : subtypes) { + assertTrue(hdlr.isWriteable(null, null, null, new MediaType(GEN_TYPE, subtype)), "writeable " + subtype); + + } + + // the remaining should be FALSE + + // null subtype + assertFalse(hdlr.isWriteable(null, null, null, new MediaType(GEN_TYPE, null))); + + // text subtype + assertFalse(hdlr.isWriteable(null, null, null, MediaType.TEXT_HTML_TYPE)); + } + + @Test + void testGetSize() { + assertEquals(-1, hdlr.getSize(null, null, null, null, null)); + } + + @Test + void testWriteTo_testReadFrom() throws Exception { + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + MyObject obj1 = new MyObject(10); + hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr); + + Object obj2 = hdlr.readFrom(CLASS_OBJ, CLASS_OBJ, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(obj1.toString(), obj2.toString()); + } + + @Test + void testWriteTo_DifferentTypes() throws Exception { + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + + // use a derived type, but specify the base type when writing + MyObject obj1 = new MyObject(10) {}; + hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr); + + Object obj2 = hdlr.readFrom(CLASS_OBJ, CLASS_OBJ, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(obj1.toString(), obj2.toString()); + } + + @Test + void testIsReadable() { + // null media type + assertTrue(hdlr.isReadable(null, null, null, null)); + + // null subtype + assertFalse(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, null))); + + for (String subtype : subtypes) { + assertTrue(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, subtype)), "readable " + subtype); + + } + + // the remaining should be FALSE + + // null subtype + assertFalse(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, null))); + + // text subtype + assertFalse(hdlr.isReadable(null, null, null, MediaType.TEXT_HTML_TYPE)); + } + + @Test + void testReadFrom_DifferentTypes() throws Exception { + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + MyObject obj1 = new MyObject(10); + hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr); + + // use a derived type, but specify the base type when reading + @SuppressWarnings("rawtypes") + Class clazz = MyObject.class; + + @SuppressWarnings("unchecked") + Class objclazz = clazz; + + Object obj2 = hdlr.readFrom(objclazz, CLASS_OBJ, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(obj1.toString(), obj2.toString()); + } + + @Test + void testMapDouble() throws Exception { + MyMap map = new MyMap(); + map.props = new HashMap<>(); + map.props.put("plainString", "def"); + map.props.put("negInt", -10); + map.props.put("doubleVal", 12.5); + map.props.put("posLong", 100000000000L); + + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + hdlr.writeTo(map, map.getClass(), map.getClass(), null, null, null, outstr); + + Object obj2 = hdlr.readFrom(Object.class, map.getClass(), null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(map.toString(), obj2.toString()); + + map = (MyMap) obj2; + + assertEquals(-10, map.props.get("negInt")); + assertEquals(100000000000L, map.props.get("posLong")); + assertEquals(12.5, map.props.get("doubleVal")); + } + + @Test + void testInterestingFields() throws IOException { + InterestingFields data = new InterestingFields(); + data.instant = Instant.ofEpochMilli(1583249713500L); + data.uuid = UUID.fromString("a850cb9f-3c5e-417c-abfd-0679cdcd1ab0"); + data.localDate = LocalDateTime.of(2020, 2, 3, 4, 5, 6, 789000000); + data.zonedDate = ZonedDateTime.of(2020, 2, 3, 4, 5, 6, 789000000, ZoneId.of("US/Eastern")); + + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + hdlr.writeTo(data, data.getClass(), data.getClass(), null, null, null, outstr); + + // ensure fields are encoded as expected + + // @formatter:off + assertThat(outstr.toString(StandardCharsets.UTF_8)) + .contains("\"2020-03-03T15:35:13.500Z\"") + .contains("\"2020-02-03T04:05:06.789\"") + .contains("\"2020-02-03T04:05:06.789-05:00[US/Eastern]\"") + .contains("a850cb9f-3c5e-417c-abfd-0679cdcd1ab0"); + // @formatter:on + + Object obj2 = hdlr.readFrom(Object.class, data.getClass(), null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(data.toString(), obj2.toString()); + } + + + @ToString + public static class MyObject { + private int id; + + public MyObject() { + super(); + } + + public MyObject(int id) { + this.id = id; + } + } + + private static class MyMap { + private Map props; + + @Override + public String toString() { + return props.toString(); + } + } + + @ToString + private static class InterestingFields { + private LocalDateTime localDate; + private Instant instant; + private UUID uuid; + private ZonedDateTime zonedDate; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java new file mode 100644 index 000000000..f2ddf06a1 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java @@ -0,0 +1,67 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.time.Instant; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class InstantAsMillisTypeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(Instant.class, new InstantAsMillisTypeAdapter()).create(); + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.instant = Instant.ofEpochMilli(1583249713500L); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("nanos").contains("1583249713500").doesNotContain("\"1583249713500\"") + .doesNotContain("T"); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null output + data.instant = null; + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"instant\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + + @ToString + private static class InterestingFields { + private Instant instant; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java new file mode 100644 index 000000000..2e27d1dcd --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java @@ -0,0 +1,64 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.Instant; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class InstantTypeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(Instant.class, new InstantTypeAdapter()).create(); + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.instant = Instant.ofEpochMilli(1583249713500L); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("nanos").contains("\"2020-03-03T15:35:13.500Z\"") + .doesNotContain("1583249713500"); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("2020", "invalid-date"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid date"); + } + + + @ToString + private static class InterestingFields { + private Instant instant; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java new file mode 100644 index 000000000..fc3c6675e --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java @@ -0,0 +1,180 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modificaitons Copyright (C) 2023-2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import java.io.Serial; +import java.lang.reflect.GenericArrayType; +import java.util.LinkedList; +import java.util.TreeMap; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class JacksonExclusionStrategyTest { + + private static JacksonExclusionStrategy strategy; + private static Gson gson; + + @BeforeAll + public static void setUpBeforeClass() { + strategy = new JacksonExclusionStrategy(); + gson = new GsonBuilder().setExclusionStrategies(strategy).create(); + } + + @Test + void testWithGson() { + Derived data = new Derived(); + data.setId(10); + data.setText("some text"); + data.setValue("some value"); + + // no fields should be serialized + String result = gson.toJson(data); + assertEquals("{}", result); + + // no fields should be deserialized + result = "{'id':20, 'text':'my text', 'value':'my value'}".replace('\'', '"'); + Derived data2 = gson.fromJson(result, Derived.class); + assertEquals(new Derived().toString(), data2.toString()); + } + + @Test + void testShouldSkipField() throws Exception { + // should skip every field of Data + assertTrue(strategy.shouldSkipField(new FieldAttributes(Data.class.getDeclaredField("id")))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(Data.class.getDeclaredField("text")))); + + // should not skip fields in Map + assertFalse(strategy.shouldSkipField(new FieldAttributes(MyMap.class.getDeclaredField("mapId")))); + } + + @Test + void testShouldSkipClass() { + assertFalse(strategy.shouldSkipClass(null)); + assertFalse(strategy.shouldSkipClass(Object.class)); + } + + @Test + void testIsManaged() { + // these classes SHOULD be managed + Class[] managed = {Data.class, Intfc.class, com.google.gson.TypeAdapter.class}; + + for (Class clazz : managed) { + assertTrue(JacksonExclusionStrategy.isManaged(clazz), clazz.getName()); + } + + // generic classes should NOT be managed + Class[] unmanaged = { + Data[].class, Enum.class, boolean.class, byte.class, short.class, int.class, + long.class, float.class, double.class, char.class, Boolean.class, Byte.class, Short.class, + Integer.class, Long.class, Float.class, Double.class, Character.class, String.class, + MyMap.class, MyList.class, MyJson.class, GenericArrayType.class}; + + for (Class clazz : unmanaged) { + assertFalse(JacksonExclusionStrategy.isManaged(clazz), clazz.getName()); + } + } + + /** + * Used to verify that no fields are exposed. + */ + @Getter + @Setter + @ToString + public static class Data { + private int id; + public String text; + + void setId(int id) { + this.id = id; + } + + void setText(String text) { + this.text = text; + } + } + + @Getter + @ToString(callSuper = true) + public static class Derived extends Data { + protected String value; + + void setValue(String value) { + this.value = value; + } + } + + /** + * Used to verify that enums are not managed. + */ + public enum Enum { + UP, DOWN, + } + + /** + * Used to verify that interfaces are managed. + */ + public interface Intfc { + int getId(); + } + + /** + * Used to verify that Maps are not managed. + */ + @Getter + public static class MyMap extends TreeMap { + @Serial + private static final long serialVersionUID = 1L; + + private int mapId; + + } + + /** + * Used to verify that Collections are not managed. + */ + public static class MyList extends LinkedList { + @Serial + private static final long serialVersionUID = 1L; + } + + /** + * Used to verify that JsonElements are not managed. + */ + @SuppressWarnings("deprecation") + public static class MyJson extends JsonElement { + @Override + public JsonElement deepCopy() { + return null; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonFieldAdapterFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonFieldAdapterFactoryTest.java new file mode 100644 index 000000000..65bf90f00 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonFieldAdapterFactoryTest.java @@ -0,0 +1,206 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.util.ArrayList; +import java.util.List; +import lombok.ToString; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +class JacksonFieldAdapterFactoryTest { + + private static JacksonFieldAdapterFactory factory = new JacksonFieldAdapterFactory(); + + private static Gson gson = new GsonBuilder().setExclusionStrategies(new JacksonExclusionStrategy()) + .registerTypeAdapterFactory(factory).create(); + + @Test + void testCreate() { + // unhandled types + assertNull(factory.create(gson, TypeToken.get(JsonElement.class))); + assertNull(factory.create(gson, TypeToken.get(NothingToSerialize.class))); + + assertNotNull(factory.create(gson, TypeToken.get(Data.class))); + assertNotNull(factory.create(gson, TypeToken.get(Derived.class))); + + Data data = new Data(); + + // deserialize using fields that aren't in the Data object + Data data2 = gson.fromJson("{\"abc\":100}", Data.class); + assertEquals(data.toString(), data2.toString()); + + // now work with valid fields + data.id = 10; + data.text = "hello"; + + String result = gson.toJson(data); + data2 = gson.fromJson(result, Data.class); + assertEquals(data.toString(), data2.toString()); + + // should also work with derived types + Derived der = new Derived(); + der.setId(20); + der.text = "world"; + der.unserialized = "abc"; + + result = gson.toJson(der); + + // should not contain the unserialized field + assertFalse(result.contains("abc")); + + Derived der2 = gson.fromJson(result, Derived.class); + der.unserialized = null; + assertEquals(der.toString(), der2.toString()); + } + + @Test + void testCreate_Lists() { + DataList lst = new DataList(); + lst.theList = new ArrayList<>(); + lst.theList.add(new Data(200, "text 20")); + lst.theList.add(new Data(210, "text 21")); + + String result = gson.toJson(lst); + assertEquals("{'theList':[{'my-id':200,'text':'text 20'},{'my-id':210,'text':'text 21'}]}".replace('\'', '"'), + result); + + DataList lst2 = gson.fromJson(result, DataList.class); + assertEquals(stripIdent(lst.toString()), stripIdent(lst2.toString())); + assertEquals(Data.class, lst2.theList.get(0).getClass()); + } + + @Test + void testCreate_OnlyOutProps() { + InFieldIgnored data = new InFieldIgnored(); + data.value = "out only"; + + // field should be serialized + String result = gson.toJson(data); + assertEquals("{'value':'out only'}".replace('\'', '"'), result); + + // field should NOT be deserialized + data = gson.fromJson(result, InFieldIgnored.class); + assertNull(data.value); + } + + @Test + void testCreate_OnlyInProps() { + OutFieldIgnored data = new OutFieldIgnored(); + data.value = "in only"; + + // field should NOT be serialized + String result = gson.toJson(data); + assertEquals("{}", result); + + // field should NOT be deserialized + data = gson.fromJson("{'value':'in only'}".replace('\'', '"'), OutFieldIgnored.class); + assertEquals("in only", data.value); + } + + /** + * Object identifiers may change with each execution, so this method is used to strip + * the identifier from the text string so that the strings will still match across + * different runs. + * + * @param text text from which to strip the identifier + * @return the text, without the identifier + */ + private String stripIdent(String text) { + return text.replaceFirst("@\\w+", "@"); + } + + @ToString + private static class Data { + @GsonJsonProperty("my-id") + private int id; + + public String text; + + public Data() { + super(); + } + + public Data(int id, String text) { + this.id = id; + this.text = text; + } + + void setId(int id) { + this.id = id; + } + } + + @ToString(callSuper = true) + private static class Derived extends Data { + // not serialized + private String unserialized; + } + + private static class DataList { + @GsonJsonProperty + private List theList; + } + + protected static class NothingToSerialize { + // not serialized + protected String unserialized; + } + + /** + * This has a field that should show up in the "output" list, but not in the "input" + * list, because the method will override it. + */ + private static class InFieldIgnored { + @GsonJsonProperty("value") + private String value; + + @GsonJsonIgnore + public void setValue(String value) { + this.value = value; + } + } + + /** + * This has a field that should show up in the "input" list, but not in the "output" + * list, because the method will override it. + */ + private static class OutFieldIgnored { + @GsonJsonProperty("value") + private String value; + + @GsonJsonIgnore + public String getValue() { + return value; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonHandlerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonHandlerTest.java new file mode 100644 index 000000000..316104c5e --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonHandlerTest.java @@ -0,0 +1,173 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import jakarta.ws.rs.core.MediaType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import lombok.ToString; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter; +import org.onap.policy.common.gson.annotation.GsonJsonAnySetter; + +class JacksonHandlerTest { + + @Test + void test() throws Exception { + JacksonHandler hdlr = new JacksonHandler(); + + assertTrue(hdlr.isReadable(null, null, null, MediaType.APPLICATION_JSON_TYPE)); + assertFalse(hdlr.isReadable(null, null, null, MediaType.TEXT_PLAIN_TYPE)); + + JsonObject expected = new JsonObject(); + expected.addProperty("myId", 100); + expected.addProperty("value", "a value"); + expected.addProperty("abc", "def"); + expected.addProperty("hello", "world"); + + Data data = new Data(); + data.id = 10; + data.value = "a value"; + data.props = new HashMap<>(); + data.props.put("abc", "def"); + data.props.put("hello", "world"); + + /* + * Ensure everything serializes as expected. + */ + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + hdlr.writeTo(data, Data.class, Data.class, null, null, null, outstr); + + StringReader rdr = new StringReader(outstr.toString("UTF-8")); + JsonObject json = new Gson().fromJson(rdr, JsonObject.class); + + assertEquals(expected, json); + + /* + * Ensure everything deserializes as expected. + */ + Data data2 = (Data) hdlr.readFrom(Object.class, Data.class, null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + + // id is not serialized, so we must copy it manually before comparing + data2.id = data.id; + + assertEquals(data.toString(), data2.toString()); + } + + @Test + void testMapDouble() throws Exception { + MyMap map = new MyMap(); + map.props = new HashMap<>(); + map.props.put("plainString", "def"); + map.props.put("negInt", -10); + map.props.put("doubleVal", 12.5); + map.props.put("posLong", 100000000000L); + + JacksonHandler hdlr = new JacksonHandler(); + ByteArrayOutputStream outstr = new ByteArrayOutputStream(); + hdlr.writeTo(map, map.getClass(), map.getClass(), null, null, null, outstr); + + Object obj2 = hdlr.readFrom(Object.class, map.getClass(), null, null, null, + new ByteArrayInputStream(outstr.toByteArray())); + assertEquals(map.toString(), obj2.toString()); + + map = (MyMap) obj2; + + assertEquals(-10, map.props.get("negInt")); + assertEquals(100000000000L, map.props.get("posLong")); + assertEquals(12.5, map.props.get("doubleVal")); + } + + /** + * This class includes all policy-specific gson annotations. + */ + @ToString + public static class Data { + protected int id; + + protected String value; + + protected Map props; + + public int getMyId() { + return 100; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @GsonJsonAnyGetter + public Map getProps() { + return props; + } + + /** + * Sets a property. + * + * @param name property name + * @param value new value + */ + @GsonJsonAnySetter + public void setProperty(String name, String value) { + if (props == null) { + props = new TreeMap<>(); + } + + props.put(name, value); + } + } + + private static class MyMap { + private Map props; + + @Override + public String toString() { + return props.toString(); + } + + @SuppressWarnings("unused") + public Map getProps() { + return props; + } + + @SuppressWarnings("unused") + public void setProps(Map props) { + this.props = props; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonMethodAdapterFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonMethodAdapterFactoryTest.java new file mode 100644 index 000000000..419969413 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonMethodAdapterFactoryTest.java @@ -0,0 +1,295 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.util.Map; +import java.util.TreeMap; +import lombok.ToString; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter; +import org.onap.policy.common.gson.annotation.GsonJsonAnySetter; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +class JacksonMethodAdapterFactoryTest { + + private static JacksonMethodAdapterFactory factory = new JacksonMethodAdapterFactory(); + + private static Gson gson = new GsonBuilder().setExclusionStrategies(new JacksonExclusionStrategy()) + .registerTypeAdapterFactory(factory).create(); + + @Test + void testCreate() { + // unhandled types + assertNull(factory.create(gson, TypeToken.get(JsonElement.class))); + assertNull(factory.create(gson, TypeToken.get(NothingToSerialize.class))); + + assertNotNull(factory.create(gson, TypeToken.get(Data.class))); + assertNotNull(factory.create(gson, TypeToken.get(Derived.class))); + assertNotNull(factory.create(gson, TypeToken.get(OnlyGetters.class))); + assertNotNull(factory.create(gson, TypeToken.get(OnlySetters.class))); + assertNotNull(factory.create(gson, TypeToken.get(OnlyAnyGetter.class))); + assertNotNull(factory.create(gson, TypeToken.get(OnlyAnySetter.class))); + + // unhandled type + + Data data = new Data(); + data.id = 10; + data.text = "some text"; + + String result = gson.toJson(data); + Data data2 = gson.fromJson(result, Data.class); + assertEquals(data.toString(), data2.toString()); + + Derived der = new Derived(); + der.setId(20); + der.setText("hello"); + der.text = "world"; + der.map = new TreeMap<>(); + der.map.put("mapA", "valA"); + der.map.put("mapB", "valB"); + + result = gson.toJson(der); + + // should not contain the unserialized fields + assertFalse(result.contains("hello")); + assertFalse(result.contains("world")); + + // null out unserialized fields + der.text = null; + + // null out overridden field + der.setText(null); + + Derived der2 = gson.fromJson(result, Derived.class); + + assertEquals(der.toString(), der2.toString()); + + // override of AnyGetter + AnyGetterOverride dblget = new AnyGetterOverride(); + dblget.setMap(der.map); + dblget.overMap = new TreeMap<>(); + dblget.overMap.put("getA", 100); + dblget.overMap.put("getB", 110); + + String result2 = gson.toJson(dblget); + dblget.overMap.keySet().forEach(key -> assertTrue(result2.contains(key), "over contains " + key)); + der.map.keySet().forEach(key -> assertFalse(result2.contains(key), "sub contains " + key)); + + // override of AnySetter + Map map = new TreeMap<>(); + map.put("setA", 200); + map.put("setB", 210); + AnySetterOverride dblset = gson.fromJson(gson.toJson(map), AnySetterOverride.class); + assertEquals(map.toString(), dblset.overMap.toString()); + assertNull(dblset.getTheMap()); + + // non-static nested class - can serialize, but not de-serialize + Container cont = new Container(500, "bye bye"); + result = gson.toJson(cont); + assertEquals("{'id':500,'nested':{'value':'bye bye'}}".replace('\'', '"'), result); + } + + @ToString + protected static class Data { + private int id; + private String text; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + // not public, but property provided + @GsonJsonProperty("text") + protected String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public void unused(String text) { + // do nothing + } + } + + @ToString(callSuper = true) + protected static class Derived extends Data { + + // overrides private field from Data + public String text; + + private Map map; + + @GsonJsonAnyGetter + public Map getTheMap() { + return map; + } + + @GsonJsonIgnore + public void setMap(Map map) { + this.map = map; + } + + @GsonJsonAnySetter + public void setMapValue(String key, String value) { + if (map == null) { + map = new TreeMap<>(); + } + + map.put(key, value); + } + } + + /** + * Has {@link GsonJsonAnyGetter} method that overrides the super class' method. + */ + private static class AnyGetterOverride extends Derived { + private Map overMap; + + @GsonJsonAnyGetter + private Map getOverride() { + return overMap; + } + } + + /** + * Has {@link GsonJsonAnySetter} method that overrides the super class' method. + */ + private static class AnySetterOverride extends Derived { + private Map overMap; + + @GsonJsonAnySetter + private void setOverride(String key, int value) { + if (overMap == null) { + overMap = new TreeMap<>(); + } + + overMap.put(key, value); + } + } + + /** + * Has nothing to serialize. + */ + protected static class NothingToSerialize { + // not serialized + protected String unserialized; + } + + /** + * Only has getters. + */ + protected static class OnlyGetters { + public int getId() { + return 1010; + } + } + + /** + * Only has setters. + */ + protected static class OnlySetters { + public void setId(int id) { + // do nothing + } + } + + /** + * Only has {@link GsonJsonAnyGetter}. + */ + private static class OnlyAnyGetter { + @GsonJsonAnyGetter + public Map getOverride() { + return null; + } + } + + /** + * Only has {@link GsonJsonAnySetter}. + */ + private static class OnlyAnySetter { + @GsonJsonAnySetter + public void setOverride(String key, int value) { + // do nothing + } + } + + /** + * Used to test serialization of non-static nested classes. + */ + @ToString + protected static class Container { + private int id; + private Nested nested; + + public Container() { + super(); + } + + public Container(int id, String value) { + this.id = id; + this.nested = new Nested(value); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Nested getNested() { + return nested; + } + + + @ToString + protected class Nested { + private String value; + + public Nested(String val) { + value = val; + } + + public String getValue() { + return value; + } + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java new file mode 100644 index 000000000..87f3c46da --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java @@ -0,0 +1,73 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.LocalDateTime; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class LocalDateTimeTypeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter()).create(); + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.date = LocalDateTime.of(2020, 2, 3, 4, 5, 6, 789000000); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("year").contains("\"2020-02-03T04:05:06.789\""); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("2020", "invalid-date"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid date"); + + // null output + data.date = null; + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"date\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + + @ToString + private static class InterestingFields { + private LocalDateTime date; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTypeAdapterTest.java new file mode 100644 index 000000000..b62a19b6a --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTypeAdapterTest.java @@ -0,0 +1,74 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.LocalDate; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class LocalDateTypeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter()).create(); + private static final String TEST_DATE = "2020-01-01"; + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.date = LocalDate.parse(TEST_DATE); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("year").contains(TEST_DATE); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("2020", "invalid-date"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid date"); + + // null output + data.date = null; + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"date\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + @ToString + private static class InterestingFields { + private LocalDate date; + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java new file mode 100644 index 000000000..048e09e58 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java @@ -0,0 +1,167 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MapDoubleAdapterFactoryTest { + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(new MapDoubleAdapterFactory()).create(); + + @Test + @SuppressWarnings("unchecked") + void testMap() { + MyMap map = new MyMap(); + map.data = new HashMap<>(); + map.data.put("plainString", "def"); + map.data.put("posInt", 10); + map.data.put("negInt", -10); + map.data.put("doubleVal", 12.5); + map.data.put("posLong", 100000000000L); + map.data.put("negLong", -100000000000L); + + Map nested = new LinkedHashMap<>(); + map.data.put("nestedMap", nested); + nested.put("nestedString", "world"); + nested.put("nestedInt", 50); + + String json = gson.toJson(map); + + map.data.clear(); + map = gson.fromJson(json, MyMap.class); + + assertEquals(json, gson.toJson(map)); + + assertEquals(10, map.data.get("posInt")); + assertEquals(-10, map.data.get("negInt")); + assertEquals(100000000000L, map.data.get("posLong")); + assertEquals(-100000000000L, map.data.get("negLong")); + assertEquals(12.5, map.data.get("doubleVal")); + assertEquals(nested, map.data.get("nestedMap")); + + nested = (Map) map.data.get("nestedMap"); + assertEquals(50, nested.get("nestedInt")); + } + + @Test + void testList() { + MyList list = new MyList(); + list.data = new ArrayList<>(); + list.data.add("ghi"); + list.data.add(100); + + List nested = new ArrayList<>(); + list.data.add(nested); + nested.add("world2"); + nested.add(500); + + String json = gson.toJson(list); + + list.data.clear(); + list = gson.fromJson(json, MyList.class); + + assertEquals(json, gson.toJson(list)); + + assertEquals("[ghi, 100, [world2, 500]]", list.data.toString()); + } + + @Test + void test_ValueIsNotObject() { + MyDoubleMap map = new MyDoubleMap(); + map.data = new LinkedHashMap<>(); + map.data.put("plainDouble", 13.5); + map.data.put("doubleAsInt", 100.0); + + String json = gson.toJson(map); + + map.data.clear(); + map = gson.fromJson(json, MyDoubleMap.class); + + // everything should still be Double - check by simply accessing + assertNotNull(map.data.get("plainDouble")); + assertNotNull(map.data.get("doubleAsInt")); + } + + @Test + void test_KeyIsNotString() { + MyObjectMap map = new MyObjectMap(); + + map.data = new LinkedHashMap<>(); + map.data.put("plainDouble2", 14.5); + map.data.put("doubleAsInt2", 200.0); + + String json = gson.toJson(map); + + map.data.clear(); + map = gson.fromJson(json, MyObjectMap.class); + + // everything should still be Double + assertEquals(14.5, map.data.get("plainDouble2")); + assertEquals(200.0, map.data.get("doubleAsInt2")); + } + + @Test + void test_ListValueIsNotObject() { + MyDoubleList list = new MyDoubleList(); + list.data = new ArrayList<>(); + list.data.add(13.5); + list.data.add(100.0); + + String json = gson.toJson(list); + + list.data.clear(); + list = gson.fromJson(json, MyDoubleList.class); + + // everything should still be Double - check by simply accessing + assertEquals("[13.5, 100.0]", list.data.toString()); + } + + private static class MyMap { + private Map data; + } + + private static class MyDoubleMap { + private Map data; + } + + private static class MyObjectMap { + private Map data; + } + + private static class MyList { + private List data; + } + + private static class MyDoubleList { + private List data; + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/OffsetDateTimeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/OffsetDateTimeAdapterTest.java new file mode 100644 index 000000000..33212b897 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/OffsetDateTimeAdapterTest.java @@ -0,0 +1,73 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.OffsetDateTime; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class OffsetDateTimeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()).create(); + private static final String TEST_DATE = "2020-01-01T12:00:00.999+05:00"; + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.date = OffsetDateTime.parse(TEST_DATE); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("year").contains(TEST_DATE); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("2020", "invalid-date"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid date"); + + // null output + data.date = null; + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"date\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + @ToString + private static class InterestingFields { + private OffsetDateTime date; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/OffsetTimeTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/OffsetTimeTypeAdapterTest.java new file mode 100644 index 000000000..27f179f1b --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/OffsetTimeTypeAdapterTest.java @@ -0,0 +1,73 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.OffsetTime; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class OffsetTimeTypeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetTime.class, new OffsetTimeTypeAdapter()).create(); + private static final String TEST_TIME = "12:00:00.999+05:00"; + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.time = OffsetTime.parse(TEST_TIME); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("foo").contains(TEST_TIME); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("12", "invalid-time"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid time"); + + // null output + data.time = null; + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"time\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + @ToString + private static class InterestingFields { + private OffsetTime time; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/StringTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/StringTypeAdapterTest.java new file mode 100644 index 000000000..a2027512d --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/StringTypeAdapterTest.java @@ -0,0 +1,95 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class StringTypeAdapterTest { + private static Gson gson = new GsonBuilder().registerTypeAdapter(MyData.class, new MyAdapter()).create(); + private static final int TEST_NUM1 = 10; + private static final int TEST_NUM3 = 30; + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.data1 = new MyData(TEST_NUM1); + data.data2 = null; + data.data3 = new MyData(TEST_NUM3); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).contains("10", "30"); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the string is invalid + String json2 = json.replace("30", "invalid-value"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid data"); + + // null output + data = new InterestingFields(); + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"data1\":null, \"data1\":null, \"data1\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // empty input + data2 = gson.fromJson("{}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + @Getter + @ToString + @AllArgsConstructor + private static class MyData { + private int num; + } + + @ToString + private static class InterestingFields { + private MyData data1; + private MyData data2; + private MyData data3; + } + + private static class MyAdapter extends StringTypeAdapter { + public MyAdapter() { + super("data", string -> new MyData(Integer.parseInt(string)), data -> String.valueOf(data.num)); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapterTest.java new file mode 100644 index 000000000..0e4e9586d --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapterTest.java @@ -0,0 +1,73 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.ZoneOffset; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class ZoneOffsetTypeAdapterTest { + private static final Gson gson = + new GsonBuilder().registerTypeAdapter(ZoneOffset.class, new ZoneOffsetTypeAdapter()).create(); + private static final String TEST_ZONE = "+05:00"; + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.zone = ZoneOffset.of(TEST_ZONE); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("foo").contains(TEST_ZONE); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("05", "invalid-zone"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid zone"); + + // null output + data.zone = null; + json = gson.toJson(data); + data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // null input + data2 = gson.fromJson("{\"zone\":null}", InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + } + + @ToString + private static class InterestingFields { + private ZoneOffset zone; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java new file mode 100644 index 000000000..bf534a5c2 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java @@ -0,0 +1,64 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +class ZonedDateTimeTypeAdapterTest { + private static Gson gson = + new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter()).create(); + + @Test + void test() { + InterestingFields data = new InterestingFields(); + data.date = ZonedDateTime.of(2020, 2, 3, 4, 5, 6, 789000000, ZoneId.of("US/Eastern")); + + String json = gson.toJson(data); + + // instant should be encoded as a number, without quotes + assertThat(json).doesNotContain("year").contains("\"2020-02-03T04:05:06.789-05:00[US/Eastern]\""); + + InterestingFields data2 = gson.fromJson(json, InterestingFields.class); + assertEquals(data.toString(), data2.toString()); + + // try when the date-time string is invalid + String json2 = json.replace("2020", "invalid-date"); + assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("invalid date"); + } + + + @ToString + private static class InterestingFields { + private ZonedDateTime date; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java new file mode 100644 index 000000000..ebe2b727f --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java @@ -0,0 +1,427 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; +import org.onap.policy.common.gson.internal.Adapter.Factory; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; +import org.onap.policy.common.gson.internal.DataAdapterFactory.DerivedData; +import org.springframework.test.util.ReflectionTestUtils; + +class AdapterTest { + private static final String GET_INVALID_NAME = "get$InvalidName"; + private static final String SET_INVALID_NAME = "set$InvalidName"; + private static final String EMPTY_ALIAS = "emptyAlias"; + private static final String GET_VALUE = ".getValue"; + private static final String GET_VALUE_NAME = "getValue"; + private static final String SET_VALUE_NAME = "setValue"; + private static final String VALUE_NAME = "value"; + private static final String MY_NAME = AdapterTest.class.getName(); + private static final String FACTORY_FIELD = "factory"; + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private static Factory saveFactory; + + /* + * The remaining fields are just used within the tests. + */ + + private String value; + + // empty alias - should use field name + @GsonJsonProperty("") + protected String emptyAlias; + + @GsonJsonProperty("name-with-alias") + protected String nameWithAlias; + + protected String unaliased; + + private List listField; + + private Data dataField; + + @BeforeAll + public static void setUpBeforeClass() { + saveFactory = (Factory) ReflectionTestUtils.getField(Adapter.class, FACTORY_FIELD); + } + + @AfterEach + void tearDown() { + ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, saveFactory); + } + + @Test + void testIsManagedField() { + assertTrue(Adapter.isManaged(field(VALUE_NAME))); + + // return an invalid field name + Factory factory = mock(Factory.class); + when(factory.getName(any(Field.class))).thenReturn("$invalidFieldName"); + ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory); + assertFalse(Adapter.isManaged(field(VALUE_NAME))); + } + + @Test + void testIsManagedMethod() { + assertTrue(Adapter.isManaged(mget(GET_VALUE_NAME))); + + // return an invalid method name + Factory factory = mock(Factory.class); + ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory); + + when(factory.getName(any(Method.class))).thenReturn(GET_INVALID_NAME); + assertFalse(Adapter.isManaged(mget(GET_VALUE_NAME))); + + when(factory.getName(any(Method.class))).thenReturn(SET_INVALID_NAME); + assertFalse(Adapter.isManaged(mset(SET_VALUE_NAME))); + } + + @Test + void testAdapterField_Converter() { + Adapter adapter = new Adapter(gson, field("dataField")); + + // first, write something of type Data + dataAdapter.reset(); + dataField = new Data(300); + JsonElement tree = adapter.toJsonTree(dataField); + assertEquals("{'id':300}".replace('\'', '"'), tree.toString()); + + // now try a subclass + dataAdapter.reset(); + dataField = new DerivedData(300, "three"); + tree = adapter.toJsonTree(dataField); + assertEquals("{'id':300,'text':'three'}".replace('\'', '"'), tree.toString()); + } + + @Test + @SuppressWarnings("unchecked") + void testAdapterField_Converter_List() { + listField = DataAdapterFactory.makeList(); + + Adapter adapter = new Adapter(gson, field("listField")); + + dataAdapter.reset(); + JsonElement tree = adapter.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + + // encode it twice so it uses the cached converter + dataAdapter.reset(); + tree = adapter.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + + dataAdapter.reset(); + List lst2 = (List) adapter.fromJsonTree(tree); + assertTrue(dataAdapter.isDataRead()); + + assertEquals(listField.toString(), lst2.toString()); + + // decode it twice so it uses the cached converter + dataAdapter.reset(); + lst2 = (List) adapter.fromJsonTree(tree); + assertTrue(dataAdapter.isDataRead()); + + assertEquals(listField.toString(), lst2.toString()); + } + + @Test + void testAdapterMethod_Converter() throws Exception { + listField = DataAdapterFactory.makeList(); + + Method getter = mget("getMyList"); + + Adapter aget = new Adapter(gson, getter, getter.getReturnType()); + + dataAdapter.reset(); + JsonElement tree = aget.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + + Method setter = AdapterTest.class.getDeclaredMethod("setMyList", List.class); + Adapter aset = new Adapter(gson, setter, setter.getGenericParameterTypes()[0]); + + dataAdapter.reset(); + @SuppressWarnings("unchecked") + List lst2 = (List) aset.fromJsonTree(tree); + assertTrue(dataAdapter.isDataRead()); + + assertEquals(listField.toString(), lst2.toString()); + } + + @Test + void testGetPropName_testGetFullName_testMakeError() { + // test field + Adapter adapter = new Adapter(gson, field(VALUE_NAME)); + + assertEquals(VALUE_NAME, adapter.getPropName()); + assertEquals(MY_NAME + ".value", adapter.getFullName()); + + + // test getter + adapter = new Adapter(gson, mget(GET_VALUE_NAME), String.class); + + assertEquals(VALUE_NAME, adapter.getPropName()); + assertEquals(MY_NAME + GET_VALUE, adapter.getFullName()); + + assertEquals("hello: " + MY_NAME + GET_VALUE, adapter.makeError("hello: ")); + + + // test setter + adapter = new Adapter(gson, mset(SET_VALUE_NAME), String.class); + + assertEquals(VALUE_NAME, adapter.getPropName()); + assertEquals(MY_NAME + ".setValue", adapter.getFullName()); + } + + @Test + void testToJsonTree() { + Adapter adapter = new Adapter(gson, field(VALUE_NAME)); + + JsonElement tree = adapter.toJsonTree("hello"); + assertTrue(tree.isJsonPrimitive()); + assertEquals("hello", tree.getAsString()); + } + + @Test + void testFromJsonTree() { + Adapter adapter = new Adapter(gson, field(VALUE_NAME)); + + assertEquals("world", adapter.fromJsonTree(new JsonPrimitive("world"))); + } + + @Test + void testDetmPropName() { + assertEquals(EMPTY_ALIAS, Adapter.detmPropName(field(EMPTY_ALIAS))); + assertEquals("name-with-alias", Adapter.detmPropName(field("nameWithAlias"))); + assertEquals("unaliased", Adapter.detmPropName(field("unaliased"))); + + // return an invalid field name + Factory factory = mock(Factory.class); + when(factory.getName(any(Field.class))).thenReturn("$invalidFieldName"); + ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory); + assertEquals(null, Adapter.detmPropName(field(VALUE_NAME))); + } + + @Test + void testDetmGetterPropName() { + assertEquals(EMPTY_ALIAS, Adapter.detmGetterPropName(mget("getEmptyAlias"))); + assertEquals("get-with-alias", Adapter.detmGetterPropName(mget("getWithAlias"))); + assertEquals("plain", Adapter.detmGetterPropName(mget("getPlain"))); + assertEquals("primBool", Adapter.detmGetterPropName(mget("isPrimBool"))); + assertEquals("boxedBool", Adapter.detmGetterPropName(mget("isBoxedBool"))); + assertEquals(null, Adapter.detmGetterPropName(mget("isString"))); + assertEquals(null, Adapter.detmGetterPropName(mget("noGet"))); + assertEquals(null, Adapter.detmGetterPropName(mget("get"))); + + // return an invalid method name + Factory factory = mock(Factory.class); + ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory); + + when(factory.getName(any(Method.class))).thenReturn(GET_INVALID_NAME); + assertEquals(null, Adapter.detmGetterPropName(mget(GET_VALUE_NAME))); + } + + @Test + void testDetmSetterPropName() { + assertEquals(EMPTY_ALIAS, Adapter.detmSetterPropName(mset("setEmptyAlias"))); + assertEquals("set-with-alias", Adapter.detmSetterPropName(mset("setWithAlias"))); + assertEquals("plain", Adapter.detmSetterPropName(mset("setPlain"))); + assertEquals(null, Adapter.detmSetterPropName(mset("noSet"))); + assertEquals(null, Adapter.detmSetterPropName(mset("set"))); + + // return an invalid method name + Factory factory = mock(Factory.class); + ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory); + + when(factory.getName(any(Method.class))).thenReturn(SET_INVALID_NAME); + assertEquals(null, Adapter.detmSetterPropName(mset(SET_VALUE_NAME))); + } + + @Test + void testGetQualifiedNameField() throws Exception { + assertEquals(MY_NAME + ".value", Adapter.getQualifiedName(AdapterTest.class.getDeclaredField(VALUE_NAME))); + } + + @Test + void testGetQualifiedNameMethod() { + assertEquals(MY_NAME + GET_VALUE, Adapter.getQualifiedName(mget(GET_VALUE_NAME))); + } + + /** + * Gets a field from this class, by name. + * + * @param name name of the field to get + * @return the field + */ + private Field field(String name) { + try { + return AdapterTest.class.getDeclaredField(name); + + } catch (SecurityException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets a "getter" method from this class, by name. + * + * @param name name of the method to get + * @return the method + */ + private Method mget(String name) { + try { + return AdapterTest.class.getDeclaredMethod(name); + + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets a "setter" method from this class, by name. + * + * @param name name of the method to get + * @return the method + */ + private Method mset(String name) { + try { + return AdapterTest.class.getDeclaredMethod(name, String.class); + + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } + + /* + * The remaining methods are just used within the tests. + */ + + protected String getValue() { + return value; + } + + // empty alias - should use method name + @GsonJsonProperty("") + protected String getEmptyAlias() { + return ""; + } + + @GsonJsonProperty("get-with-alias") + protected String getWithAlias() { + return ""; + } + + // no alias, begins with "get" + protected String getPlain() { + return ""; + } + + // begins with "is", returns primitive boolean + protected boolean isPrimBool() { + return true; + } + + // begins with "is", returns boxed Boolean + protected Boolean isBoxedBool() { + return true; + } + + // begins with "is", but doesn't return a boolean + protected String isString() { + return ""; + } + + // doesn't begin with "get" + protected String noGet() { + return ""; + } + + // nothing after "get" + protected String get() { + return ""; + } + + + protected void setValue(String text) { + // do nothing + } + + // empty alias - should use method name + @GsonJsonProperty("") + protected void setEmptyAlias(String text) { + // do nothing + } + + @GsonJsonProperty("set-with-alias") + protected void setWithAlias(String text) { + // do nothing + } + + // no alias, begins with "set" + protected void setPlain(String text) { + // do nothing + } + + // doesn't begin with "set" + protected void noSet(String text) { + // do nothing + } + + // nothing after "get" + protected void set(String text) { + // do nothing + } + + // returns a list + protected List getMyList() { + return listField; + } + + // accepts a list + protected void setMyList(List newList) { + listField = newList; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnyGetterSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnyGetterSerializerTest.java new file mode 100644 index 000000000..1e206cf5c --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnyGetterSerializerTest.java @@ -0,0 +1,134 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; + +class AnyGetterSerializerTest { + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private Set set; + private AnyGetterSerializer ser; + + /** + * Set up. + * + * @throws Exception if an error occurs + */ + @BeforeEach + void setUp() throws Exception { + set = new HashSet<>(Arrays.asList("id", "value")); + ser = new AnyGetterSerializer(gson, set, MapData.class.getDeclaredMethod("getTheMap")); + } + + @Test + void testAddToTree_testCopyLiftedItems() { + JsonObject tree = new JsonObject(); + tree.addProperty("hello", "world"); + + MapData data = new MapData(); + + data.map = DataAdapterFactory.makeMap(); + + // this should not be copied because it is in the "set" + data.map.put("value", Arrays.asList(new Data(1000))); + + dataAdapter.reset(); + JsonObject tree2 = tree.deepCopy(); + ser.addToTree(data, tree2); + + assertTrue(dataAdapter.isDataWritten()); + + DataAdapterFactory.addToObject(tree); + + assertEquals(tree.toString(), tree2.toString()); + } + + @Test + void testAddToTree_NullMap() { + JsonObject tree = new JsonObject(); + tree.addProperty("hello", "world"); + + MapData data = new MapData(); + + // leave "map" unset + + JsonObject tree2 = tree.deepCopy(); + ser.addToTree(data, tree2); + + assertEquals(tree.toString(), tree2.toString()); + } + + @Test + void testAddToTree_NotAnObject() throws Exception { + ser = new AnyGetterSerializer(gson, set, NotAnObject.class.getDeclaredMethod("getNonMap")); + + JsonObject tree = new JsonObject(); + + NotAnObject data = new NotAnObject(); + data.text = "bye bye"; + + assertThatThrownBy(() -> ser.addToTree(data, tree)).isInstanceOf(JsonParseException.class) + .hasMessage(AnyGetterSerializer.NOT_AN_OBJECT_ERR + NotAnObject.class.getName() + ".getNonMap"); + } + + public static class MapData { + protected int id; + protected String value; + protected Map> map; + + protected Map> getTheMap() { + return map; + } + } + + /** + * The "lifted" property is not a JsonObject so it should throw an exception. + */ + public static class NotAnObject { + protected String text; + + public String getNonMap() { + return text; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnySetterDeserializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnySetterDeserializerTest.java new file mode 100644 index 000000000..d833925b4 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnySetterDeserializerTest.java @@ -0,0 +1,94 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; + +class AnySetterDeserializerTest { + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private Set set; + private AnySetterDeserializer deser; + + /** + * Set up. + * + * @throws Exception if an error occurs + */ + @BeforeEach + void setUp() throws Exception { + set = new HashSet<>(Arrays.asList("id", "value")); + deser = new AnySetterDeserializer(gson, set, + MapData.class.getDeclaredMethod("setItem", String.class, List.class)); + } + + @Test + void testAnySetterDeserializer() { + JsonObject json = new JsonObject(); + + // these should not be copied + json.addProperty("id", 10); + json.addProperty("value", "the-value"); + + // these should be copied + DataAdapterFactory.addToObject(json); + + MapData data = new MapData(); + data.map = new TreeMap<>(); + + dataAdapter.reset(); + deser.getFromTree(json, data); + + assertTrue(dataAdapter.isDataRead()); + assertNotNull(data.map); + assertEquals(DataAdapterFactory.makeMap().toString(), data.map.toString()); + } + + public static class MapData { + protected Map> map; + + protected void setItem(String key, List value) { + map.put(key, value); + } + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java new file mode 100644 index 000000000..007724aed --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java @@ -0,0 +1,505 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.gson.JsonParseException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; +import lombok.Getter; +import lombok.Setter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter; +import org.onap.policy.common.gson.annotation.GsonJsonAnySetter; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.gson.annotation.GsonJsonProperty; + +class ClassWalkerTest { + + private static final String SET_OVERRIDE = ".setOverride"; + private static final String INVALID_FIELD_NAME = "invalidFieldName"; + + private MyWalker walker; + + /** + * Set up. + */ + @BeforeEach + void setUp() { + walker = new MyWalker(); + } + + @Test + void testExamineClassOfQ_testExamineField_testExamineInField_testExamineOutField() { + walker.walkClassHierarchy(DerivedFromBottom.class); + + assertEquals("[InterfaceOne, InterfaceTwo, InterfaceOne, InterfaceThree, Bottom, DerivedFromBottom]", + walker.classes.toString()); + + List inFields = walker.getInProps(Field.class).stream().map(Field::getName).sorted().toList(); + assertEquals("[exposedField, overriddenValue, transField]", inFields.toString()); + + List outFields = walker.getInProps(Field.class).stream().map(Field::getName).sorted().toList(); + assertEquals("[exposedField, overriddenValue, transField]", outFields.toString()); + + // should work with interfaces without throwing an NPE + walker.walkClassHierarchy(InterfaceOne.class); + } + + @Test + void testHasAnyGetter() { + walker.walkClassHierarchy(Object.class); + assertNull(walker.getAnyGetter()); + assertNull(walker.getAnySetter()); + + walker.walkClassHierarchy(AnyGetterIgnored.class); + assertNull(walker.getAnyGetter()); + assertNull(walker.getAnySetter()); + + walker.walkClassHierarchy(AnyGetterOnly.class); + assertNotNull(walker.getAnyGetter()); + assertNull(walker.getAnySetter()); + } + + @Test + void testHasAnySetter() { + walker.walkClassHierarchy(Object.class); + assertNull(walker.getAnySetter()); + assertNull(walker.getAnyGetter()); + + walker.walkClassHierarchy(AnySetterIgnored.class); + assertNull(walker.getAnySetter()); + assertNull(walker.getAnyGetter()); + + walker.walkClassHierarchy(AnySetterOnly.class); + assertNotNull(walker.getAnySetter()); + assertNull(walker.getAnyGetter()); + } + + @Test + void testExamineMethod() { + walker.walkClassHierarchy(DerivedFromData.class); + + assertEquals("[Data, DerivedFromData]", walker.classes.toString()); + + // ensure all methods were examined + Collections.sort(walker.methods); + List lst = Arrays.asList("getId", "getValue", "getOnlyOut", "getStatic", "getText", "getTheMap", + "getUnserialized", "getValue", "getWithParams", "setExtraParams", "setId", "setMap", + "setMapValue", "setMissingParams", "setNonPublic", "setOnlyIn", "setText", "setUnserialized", + "setValue", "setValue", "wrongGetPrefix", "wrongSetPrefix"); + Collections.sort(lst); + assertEquals(lst.toString(), walker.methods.toString()); + + assertNotNull(walker.getAnyGetter()); + assertEquals("getTheMap", walker.getAnyGetter().getName()); + + List getters = walker.getOutProps(Method.class).stream().map(Method::getName).sorted().toList(); + assertEquals("[getId, getOnlyOut, getValue]", getters.toString()); + + assertNotNull(walker.getAnySetter()); + assertEquals("setMapValue", walker.getAnySetter().getName()); + + List setters = walker.getInProps(Method.class).stream().map(Method::getName).sorted().toList(); + assertEquals("[setId, setOnlyIn, setValue]", setters.toString()); + + // getter with invalid parameter count + assertThatThrownBy(() -> walker.walkClassHierarchy(AnyGetterMismatchParams.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_GETTER_MISMATCH_ERR + + AnyGetterMismatchParams.class.getName() + ".getTheMap"); + + // setter with too few parameters + assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterTooFewParams.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_MISMATCH_ERR + + AnySetterTooFewParams.class.getName() + SET_OVERRIDE); + + // setter with too many parameters + assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterTooManyParams.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_MISMATCH_ERR + + AnySetterTooManyParams.class.getName() + SET_OVERRIDE); + + // setter with invalid parameter type + assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterInvalidParam.class)) + .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_TYPE_ERR + + AnySetterInvalidParam.class.getName() + SET_OVERRIDE); + } + + @Test + void testExamineMethod_AnyGetter() { + walker.walkClassHierarchy(AnyGetterOverride.class); + + assertNotNull(walker.getAnyGetter()); + assertEquals("getOverride", walker.getAnyGetter().getName()); + } + + @Test + void testExamineMethod_AnySetter() { + walker.walkClassHierarchy(AnySetterOverride.class); + + assertNotNull(walker.getAnySetter()); + assertEquals("setOverride", walker.getAnySetter().getName()); + } + + @Test + void testGetInNotIgnored_testGetOutNotIgnored() { + walker.walkClassHierarchy(DerivedFromData.class); + + assertEquals("[id, onlyIn, text, value]", new TreeSet<>(walker.getInNotIgnored()).toString()); + assertEquals("[id, onlyOut, text, value]", new TreeSet<>(walker.getOutNotIgnored()).toString()); + } + + /** + * Walker subclass that records items that are examined. + */ + private static class MyWalker extends ClassWalker { + private final List classes = new ArrayList<>(); + private final List methods = new ArrayList<>(); + + @Override + protected void examine(Class clazz) { + classes.add(clazz.getSimpleName()); + + super.examine(clazz); + } + + @Override + protected void examine(Method method) { + if (Adapter.isManaged(method)) { + methods.add(method.getName()); + } + + super.examine(method); + } + + @Override + protected String detmPropName(Field field) { + if (INVALID_FIELD_NAME.equals(field.getName())) { + return null; + } + + return super.detmPropName(field); + } + } + + protected interface InterfaceOne { + int id = 1000; // NOSONAR I think this is meant to be accessible as fields, not constants + } + + protected interface InterfaceTwo { + String text = "intfc2-text"; // NOSONAR I think this is meant to be accessible as fields, not constants + } + + private interface InterfaceThree { + + } + + protected static class Bottom implements InterfaceOne, InterfaceThree { + private int id; + public String value; + + // this is not actually invalid, but will be treated as if it were + public String invalidFieldName; + + @GsonJsonProperty("exposed") + private String exposedField; + + @GsonJsonIgnore + public int ignored; + + public transient int ignoredTransField; + + @GsonJsonProperty("trans") + public transient int transField; + + @GsonJsonIgnore + public int getId() { + return id; + } + + @GsonJsonIgnore + public void setId(int id) { + this.id = id; + } + } + + protected static class DerivedFromBottom extends Bottom implements InterfaceOne, InterfaceTwo { + private String text; + protected String anotherValue; + + @GsonJsonProperty("value") + public String overriddenValue; + + @GsonJsonIgnore + public String getText() { + return text; + } + + @GsonJsonIgnore + public void setText(String text) { + this.text = text; + } + } + + @Setter + protected static class Data { + @Getter + private int id; + // this will be ignored, because there's already a field by this name + private String text; + + // not public, but property provided + @GsonJsonProperty("text") + protected String getText() { + return text; + } + + // should only show up in the output list + public int getOnlyOut() { + return 1100; + } + + // will be overridden by subclass + @GsonJsonProperty("super-value-getter") + public String getValue() { + return null; + } + + // will be overridden by subclass + @GsonJsonProperty("super-value-setter") + public void setValue(String value) { + // do nothing + } + } + + protected static class DerivedFromData extends Data { + // not serialized + private String unserialized; + + // overrides private field and public method from Data + public String text; + + private Map map; + + private String value; + + @Override + public String getValue() { + return value; + } + + @Override + public void setValue(String value) { + this.value = value; + } + + @GsonJsonAnyGetter + public Map getTheMap() { + return map; + } + + @GsonJsonIgnore + public void setMap(Map map) { + this.map = map; + } + + @GsonJsonAnySetter + public void setMapValue(String key, String value) { + if (map == null) { + map = new TreeMap<>(); + } + + map.put(key, value); + } + + @GsonJsonIgnore + public String getUnserialized() { + return unserialized; + } + + @GsonJsonIgnore + public void setUnserialized(String unserialized) { + this.unserialized = unserialized; + } + + // should only show up in the input list + public void setOnlyIn(int value) { + // do nothing + } + + // has a param - shouldn't be serialized + public int getWithParams(String text) { + return 1000; + } + + // too few params - shouldn't be serialized + public void setMissingParams() { + // do nothing + } + + // too many params - shouldn't be serialized + public void setExtraParams(String text, String moreText) { + // do nothing + } + + // not public - shouldn't be serialized + protected void setNonPublic(String text) { + // do nothing + } + + // doesn't start with "get" + public String wrongGetPrefix() { + return null; + } + + // doesn't start with "set" + public void wrongSetPrefix(String text) { + // do nothing + } + + // static + public static String getStatic() { + return null; + } + } + + /** + * The "get" method has an incorrect argument count. + */ + private static class AnyGetterMismatchParams { + @GsonJsonAnyGetter + public Map getTheMap(String arg) { + return new TreeMap<>(); + } + } + + /** + * Has {@link GsonJsonAnyGetter} method. + */ + private static class AnyGetterOnly { + @GsonJsonAnyGetter + private Map getOverride() { + return null; + } + } + + /** + * Has {@link GsonJsonAnyGetter} method, but it's ignored. + */ + private static class AnyGetterIgnored { + @GsonJsonAnyGetter + @GsonJsonIgnore + private Map getOverride() { + return null; + } + } + + /** + * Has {@link GsonJsonAnySetter} method. + */ + private static class AnySetterOnly { + @GsonJsonAnySetter + private void setOverride(String key, int value) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnySetter} method, but it's ignored. + */ + private static class AnySetterIgnored { + @GsonJsonAnySetter + @GsonJsonIgnore + private void setOverride(String key, int value) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnyGetter} method that overrides the super class' method. + */ + private static class AnyGetterOverride extends DerivedFromData { + private Map overMap; + + @GsonJsonAnyGetter + private Map getOverride() { + return overMap; + } + } + + /** + * Has {@link GsonJsonAnySetter} method that overrides the super class' method. + */ + private static class AnySetterOverride extends DerivedFromData { + private Map overMap; + + @GsonJsonAnySetter + private void setOverride(String key, int value) { + if (overMap == null) { + overMap = new TreeMap<>(); + } + + overMap.put(key, value); + } + } + + /** + * Has {@link GsonJsonAnySetter} method with too few parameters. + */ + private static class AnySetterTooFewParams extends DerivedFromData { + @GsonJsonAnySetter + public void setOverride(String key) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnySetter} method with too few parameters. + */ + private static class AnySetterTooManyParams extends DerivedFromData { + @GsonJsonAnySetter + public void setOverride(String key, int value, String anotherValue) { + // do nothing + } + } + + /** + * Has {@link GsonJsonAnySetter} method whose first argument type is incorrect. + */ + private static class AnySetterInvalidParam extends DerivedFromData { + @GsonJsonAnySetter + public void setOverride(Integer key, String value) { + // do nothing + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java new file mode 100644 index 000000000..d2cdf7f8e --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java @@ -0,0 +1,304 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import lombok.ToString; + +/** + * Factory used with test Data. + */ +public class DataAdapterFactory implements TypeAdapterFactory { + + /** + * Output of {@link #makeList()}, encoded as json. + */ + public static final String ENCODED_LIST = "[{'id':100},{'id':101}]".replace('\'', '"'); + + /** + * Output of {@link #makeMap()}, encoded as json. + */ + public static final String ENCODED_MAP = "'data-100':{'id':100},'data-101':{'id':101}".replace('\'', '"'); + + /** + * Object handled by this factory. + */ + @ToString + public static class Data { + private int id; + + public Data() { + super(); + } + + public Data(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + } + + /** + * Object derived from Data. + */ + @ToString(callSuper = true) + public static class DerivedData extends Data { + private String text; + + public DerivedData() { + super(); + } + + public DerivedData(int id, String text) { + super(id); + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + /** + * Set to {@code true} when {@link #write(JsonWriter, Data)} has been invoked. + */ + private boolean dataWritten = false; + + /** + * Set to {@code true} when {@link #read(JsonReader)} has been invoked. + */ + private boolean dataRead = false; + + /** + * Clears the flags that indicate that "read" or "write" has been invoked. + */ + public void reset() { + dataWritten = true; + dataRead = true; + } + + public boolean isDataWritten() { + return dataWritten; + } + + public boolean isDataRead() { + return dataRead; + } + + /** + * Makes a list of Data. + * + * @return a new list of Data + */ + public static List makeList() { + List listField = new ArrayList<>(); + + listField.add(new Data(100)); + listField.add(new Data(101)); + + return listField; + } + + /** + * Makes an array of Data. + * + * @return a new array of Data + */ + public static JsonArray makeArray() { + JsonArray arr = new JsonArray(); + + for (Data data : makeList()) { + JsonObject json = new JsonObject(); + json.addProperty("id", data.getId()); + arr.add(json); + } + + return arr; + } + + /** + * Makes a map of Data. + * + * @return a new map of Data + */ + public static Map> makeMap() { + Map> map = new TreeMap<>(); + + for (Data data : makeList()) { + map.put("data-" + data.getId(), Arrays.asList(data)); + } + + return map; + } + + /** + * Adds Data objects to a tree, mirroring {@link #makeMap()}. + * + * @param tree tree into which objects are to be added + */ + public static void addToObject(JsonObject tree) { + for (JsonElement ent : makeArray()) { + JsonObject obj = ent.getAsJsonObject(); + JsonArray arr = new JsonArray(); + arr.add(obj); + tree.add("data-" + obj.get("id").getAsString(), arr); + } + } + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() == Data.class) { + return (TypeAdapter) new DataTypeAdapter(gson.getDelegateAdapter(this, TypeToken.get(Data.class)), + gson.getAdapter(JsonElement.class)); + } + + if (type.getRawType() == DerivedData.class) { + return (TypeAdapter) new DerivedDataTypeAdapter( + gson.getDelegateAdapter(this, TypeToken.get(DerivedData.class)), + gson.getAdapter(JsonElement.class)); + } + + return null; + } + + /** + * Adapter for "Data". + */ + private class DataTypeAdapter extends TypeAdapter { + private TypeAdapter delegate; + private TypeAdapter elementAdapter; + + /** + * Constructs the object. + * + * @param delegate delegate adapter + * @param elementAdapter element adapter + */ + public DataTypeAdapter(TypeAdapter delegate, TypeAdapter elementAdapter) { + this.delegate = delegate; + this.elementAdapter = elementAdapter; + } + + @Override + public void write(JsonWriter out, Data data) throws IOException { + dataWritten = true; + + JsonElement tree = delegate.toJsonTree(data); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + jsonObj.addProperty("id", data.getId()); + } + + elementAdapter.write(out, tree); + } + + @Override + public Data read(JsonReader in) throws IOException { + dataRead = true; + + JsonElement tree = elementAdapter.read(in); + Data data = delegate.fromJsonTree(tree); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + data.setId(jsonObj.get("id").getAsInt()); + } + + return data; + } + } + + /** + * Adapter for "DerivedData". + */ + private class DerivedDataTypeAdapter extends TypeAdapter { + private TypeAdapter delegate; + private TypeAdapter elementAdapter; + + /** + * Constructs the object. + * + * @param delegate delegate adapter + * @param elementAdapter element adapter + */ + public DerivedDataTypeAdapter(TypeAdapter delegate, TypeAdapter elementAdapter) { + this.delegate = delegate; + this.elementAdapter = elementAdapter; + } + + @Override + public void write(JsonWriter out, DerivedData data) throws IOException { + dataWritten = true; + + JsonElement tree = delegate.toJsonTree(data); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + jsonObj.addProperty("id", data.getId()); + jsonObj.addProperty("text", data.getText()); + } + + elementAdapter.write(out, tree); + } + + @Override + public DerivedData read(JsonReader in) throws IOException { + dataRead = true; + + JsonElement tree = elementAdapter.read(in); + DerivedData data = delegate.fromJsonTree(tree); + + if (tree.isJsonObject()) { + JsonObject jsonObj = tree.getAsJsonObject(); + data.setId(jsonObj.get("id").getAsInt()); + data.setText(jsonObj.get("text").getAsString()); + } + + return data; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldDeserializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldDeserializerTest.java new file mode 100644 index 000000000..8f783bcb0 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldDeserializerTest.java @@ -0,0 +1,109 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; + +class FieldDeserializerTest { + private static final String TEXT_FIELD_NAME = "text"; + private static final String LIST_FIELD_NAME = "listField"; + private static final String INITIAL_VALUE = "initial value"; + private static final String NEW_VALUE = "new value"; + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private FieldDeserializer deser; + + private String text; + + private List listField; + + @Test + void testGetFromTree() throws Exception { + deser = new FieldDeserializer(gson, FieldDeserializerTest.class.getDeclaredField(TEXT_FIELD_NAME)); + + JsonObject json = new JsonObject(); + + // no value in tree - text remains unchanged + text = INITIAL_VALUE; + deser.getFromTree(json, this); + assertEquals(INITIAL_VALUE, text); + + // null value in tree - text remains unchanged + json.add(TEXT_FIELD_NAME, JsonNull.INSTANCE); + deser.getFromTree(json, this); + assertEquals(INITIAL_VALUE, text); + + // now assign a value - text should be changed now + json.addProperty(TEXT_FIELD_NAME, NEW_VALUE); + + deser.getFromTree(json, this); + assertEquals(NEW_VALUE, text); + + /* + * check list field + */ + deser = new FieldDeserializer(gson, FieldDeserializerTest.class.getDeclaredField(LIST_FIELD_NAME)); + + json.add(LIST_FIELD_NAME, DataAdapterFactory.makeArray()); + + dataAdapter.reset(); + listField = null; + deser.getFromTree(json, this); + + assertTrue(dataAdapter.isDataRead()); + assertEquals(DataAdapterFactory.makeList().toString(), listField.toString()); + } + + @Test + void testGetFromTree_SetEx() throws Exception { + deser = new FieldDeserializer(gson, FieldDeserializerTest.class.getDeclaredField(TEXT_FIELD_NAME)) { + @Override + public Object fromJsonTree(JsonElement tree) { + // return an int, which won't fit in a String - cause an exception + return 10; + } + }; + + JsonObject json = new JsonObject(); + json.addProperty(TEXT_FIELD_NAME, NEW_VALUE); + + assertThatThrownBy(() -> deser.getFromTree(json, this)).isInstanceOf(JsonParseException.class) + .hasMessage(FieldDeserializer.SET_ERR + FieldDeserializerTest.class.getName() + ".text"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldSerializerTest.java new file mode 100644 index 000000000..1431f47be --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldSerializerTest.java @@ -0,0 +1,98 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; + +class FieldSerializerTest { + private static final String TEXT_FIELD_NAME = "text"; + private static final String LIST_FIELD_NAME = "listField"; + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private FieldSerializer ser; + + protected String text; + + private List listField; + + @Test + void testAddToTree() throws Exception { + ser = new FieldSerializer(gson, FieldSerializerTest.class.getDeclaredField(TEXT_FIELD_NAME)); + + // serialize null value first + text = null; + + JsonObject json = new JsonObject(); + ser.addToTree(this, json); + assertTrue(json.get(TEXT_FIELD_NAME).isJsonNull()); + + // serialize an actual value + text = "hello"; + ser.addToTree(this, json); + assertEquals("hello", json.get(TEXT_FIELD_NAME).getAsString()); + + /* + * check list field + */ + listField = DataAdapterFactory.makeList(); + + ser = new FieldSerializer(gson, FieldSerializerTest.class.getDeclaredField(LIST_FIELD_NAME)); + + dataAdapter.reset(); + JsonElement tree = ser.toJsonTree(listField); + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + } + + @Test + void testAddToTree_GetEx() throws Exception { + ser = new FieldSerializer(gson, FieldSerializerTest.class.getDeclaredField(TEXT_FIELD_NAME)) { + @Override + protected Object getFromObject(Object source) throws IllegalAccessException { + throw new IllegalAccessException("expected exception"); + } + }; + + text = "world"; + + JsonObject obj = new JsonObject(); + + assertThatThrownBy(() -> ser.addToTree(this, obj)).isInstanceOf(JsonParseException.class) + .hasMessage(FieldSerializer.GET_ERR + FieldSerializerTest.class.getName() + ".text"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/JacksonTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/JacksonTypeAdapterTest.java new file mode 100644 index 000000000..a5ebc1edc --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/JacksonTypeAdapterTest.java @@ -0,0 +1,214 @@ +/* + * ============LICENSE_START============================================================== + * ONAP + * ======================================================================================= + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved. + * ======================================================================================= + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END================================================================ + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import lombok.ToString; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JacksonTypeAdapterTest { + private static final String HELLO = "hello"; + private static final String WORLD = "world"; + + /** + * Gson object that excludes fields, as we're going to process the fields ourselves. + */ + private static final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + private JacksonTypeAdapter adapter; + private List sers; + private List desers; + + /** + * Initializes the previously defined fields. + */ + @BeforeEach + void setUp() { + // create list of serializers, one for "id" and one for "value" + sers = new ArrayList<>(2); + sers.add(new NamedSer(HELLO) { + @Override + protected String getValue(Data data) { + return data.id; + } + }); + sers.add(new NamedSer(WORLD) { + @Override + protected String getValue(Data data) { + return data.value; + } + }); + + // create list of deserializers, one for "id" and one for "value" + desers = new ArrayList<>(2); + desers.add(new NamedDeser(HELLO) { + @Override + protected void setValue(Data data, String value) { + data.id = value; + } + }); + desers.add(new NamedDeser(WORLD) { + @Override + protected void setValue(Data data, String value) { + data.value = value; + } + }); + + TypeAdapterFactory factory = new TypeAdapterFactory() { + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + return null; + } + }; + + TypeAdapter delegate = gson.getDelegateAdapter(factory, TypeToken.get(Data.class)); + + adapter = new JacksonTypeAdapter<>(gson, delegate, sers, desers); + } + + @Test + void testWriteJsonWriterT() throws Exception { + Data data = new Data("abc", "def"); + + StringWriter wtr = new StringWriter(); + adapter.write(new JsonWriter(wtr), data); + + assertEquals("{'hello':'abc','world':'def'}".replace('\'', '"'), wtr.toString()); + } + + /** + * Tests the case where the delegate does not return a JsonObject. + * + * @throws Exception if an error occurs + */ + @Test + void testWriteJsonWriterT_NotAnObject() throws Exception { + TypeAdapter delegate = gson.getAdapter(String.class); + JacksonTypeAdapter stringAdapter = new JacksonTypeAdapter<>(gson, delegate, sers, desers); + + StringWriter wtr = new StringWriter(); + stringAdapter.write(new JsonWriter(wtr), "write text"); + + assertEquals("'write text'".replace('\'', '"'), wtr.toString()); + } + + @Test + void testReadJsonReader() throws Exception { + Data data = adapter + .read(new JsonReader(new StringReader("{'hello':'four','world':'score'}".replace('\'', '"')))); + + assertEquals(new Data("four", "score").toString(), data.toString()); + } + + /** + * Tests the case where the delegate does not use a JsonObject. + * + * @throws Exception if an error occurs + */ + @Test + void testReadJsonReader_NotAnObject() throws Exception { + TypeAdapter delegate = gson.getAdapter(String.class); + JacksonTypeAdapter stringAdapter = new JacksonTypeAdapter<>(gson, delegate, sers, desers); + + String data = stringAdapter.read(new JsonReader(new StringReader("'read text'".replace('\'', '"')))); + + assertEquals("read text", data); + } + + @ToString + private static class Data { + private String id; + private String value; + + /* + * This is invoked by gson via reflection, thus no direct invocation. Hence it has + * to be labeled "unused". + */ + @SuppressWarnings("unused") + public Data() { + super(); + } + + public Data(String id, String value) { + this.id = id; + this.value = value; + } + } + + private abstract static class NamedSer implements Serializer { + private final String name; + + /** + * Constructs the object. + * + * @param name the name of the field, when stored in a JsonObject + */ + public NamedSer(String name) { + this.name = name; + } + + @Override + public void addToTree(Object source, JsonObject target) { + Data data = (Data) source; + target.addProperty(name, getValue(data)); + } + + protected abstract String getValue(Data data); + } + + private abstract static class NamedDeser implements Deserializer { + private final String name; + + /** + * Constructs the object. + * + * @param name the name of the field, when stored in a JsonObject + */ + public NamedDeser(String name) { + this.name = name; + } + + @Override + public void getFromTree(JsonObject source, Object target) { + Data data = (Data) target; + setValue(data, source.get(name).getAsString()); + } + + protected abstract void setValue(Data data, String value); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/LifterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/LifterTest.java new file mode 100644 index 000000000..e83757b67 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/LifterTest.java @@ -0,0 +1,55 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class LifterTest { + + private static Gson gson = new Gson(); + + @Test + void testLifter_testShouldLift() throws Exception { + Set set = new HashSet<>(Arrays.asList("abc", "def")); + Lifter lifter = new Lifter(gson, set, LifterTest.class.getDeclaredMethod("getValue"), String.class); + + // should not lift these + assertFalse(lifter.shouldLift("abc")); + assertFalse(lifter.shouldLift("def")); + + // should lift anything else + assertTrue(lifter.shouldLift("hello")); + assertTrue(lifter.shouldLift("world")); + } + + public String getValue() { + return ""; + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodAdapterTest.java new file mode 100644 index 000000000..17a184a33 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodAdapterTest.java @@ -0,0 +1,58 @@ +/*-- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import org.junit.jupiter.api.Test; + +class MethodAdapterTest { + private static final Gson gson = new Gson(); + + private String saved; + + @Test + void testMethodAdapter_testInvoke() throws Exception { + MethodAdapter adapter = + new MethodAdapter(gson, MethodAdapterTest.class.getDeclaredMethod("getValue"), String.class); + assertEquals("hello", adapter.invoke(this)); + + MethodAdapter adapter2 = new MethodAdapter(gson, + MethodAdapterTest.class.getDeclaredMethod("setValue", String.class), String.class); + adapter2.invoke(this, "world"); + assertEquals("world", saved); + + assertThatThrownBy(() -> adapter2.invoke(this, 100)).isInstanceOf(JsonParseException.class) + .hasMessage(MethodAdapter.INVOKE_ERR + MethodAdapterTest.class.getName() + ".setValue"); + } + + public String getValue() { + return "hello"; + } + + void setValue(String val) { + saved = val; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodDeserializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodDeserializerTest.java new file mode 100644 index 000000000..60fe7a0a3 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodDeserializerTest.java @@ -0,0 +1,100 @@ +/*-- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; + +class MethodDeserializerTest { + private static final String PROP_NAME = "text"; + private static final String METHOD_NAME = "setText"; + private static final String INITIAL_VALUE = "initial value"; + private static final String NEW_VALUE = "new value"; + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private MethodDeserializer deser; + + private String text; + + private List listField; + + @Test + void testGetFromTree() throws Exception { + deser = new MethodDeserializer(gson, MethodDeserializerTest.class.getDeclaredMethod(METHOD_NAME, String.class)); + + // non-existent value - should not overwrite + text = INITIAL_VALUE; + JsonObject json = new JsonObject(); + deser.getFromTree(json, this); + assertEquals(INITIAL_VALUE, text); + + // null value - should not overwrite + text = INITIAL_VALUE; + json.add(PROP_NAME, JsonNull.INSTANCE); + deser.getFromTree(json, this); + assertEquals(INITIAL_VALUE, text); + + // has a value - should store it + text = INITIAL_VALUE; + json.addProperty(PROP_NAME, NEW_VALUE); + deser.getFromTree(json, this); + assertEquals(NEW_VALUE, text); + + /* + * check list field + */ + deser = new MethodDeserializer(gson, MethodDeserializerTest.class.getDeclaredMethod("setTheList", List.class)); + + json = new JsonObject(); + json.add("theList", DataAdapterFactory.makeArray()); + + dataAdapter.reset(); + listField = null; + deser.getFromTree(json, this); + + assertTrue(dataAdapter.isDataRead()); + assertNotNull(listField); + assertEquals(DataAdapterFactory.makeList().toString(), listField.toString()); + } + + protected void setText(String text) { + this.text = text; + } + + protected void setTheList(List lst) { + listField = lst; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodSerializerTest.java new file mode 100644 index 000000000..ae41e08d3 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodSerializerTest.java @@ -0,0 +1,88 @@ +/*-- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.gson.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.gson.JacksonExclusionStrategy; +import org.onap.policy.common.gson.internal.DataAdapterFactory.Data; + +class MethodSerializerTest { + private static final String PROP_NAME = "text"; + private static final String METHOD_NAME = "getText"; + + private static DataAdapterFactory dataAdapter = new DataAdapterFactory(); + + private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter) + .setExclusionStrategies(new JacksonExclusionStrategy()).create(); + + private MethodSerializer ser; + + private String text; + + private List listField; + + @Test + void testAddToTree() throws Exception { + ser = new MethodSerializer(gson, MethodSerializerTest.class.getDeclaredMethod(METHOD_NAME)); + + // serialize null value first + text = null; + + JsonObject json = new JsonObject(); + ser.addToTree(this, json); + assertTrue(json.get(PROP_NAME).isJsonNull()); + + // serialize an actual value + text = "hello"; + ser.addToTree(this, json); + assertEquals("hello", json.get(PROP_NAME).getAsString()); + + /* + * check list field + */ + listField = DataAdapterFactory.makeList(); + + ser = new MethodSerializer(gson, MethodSerializerTest.class.getDeclaredMethod("getTheList")); + + dataAdapter.reset(); + JsonElement tree = ser.toJsonTree(listField); + + assertTrue(dataAdapter.isDataWritten()); + assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString()); + } + + protected String getText() { + return text; + } + + protected List getTheList() { + return listField; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java new file mode 100644 index 000000000..ecd2f20cd --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java @@ -0,0 +1,111 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2024 Nordix Foundation. + * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.onap.policy.common.parameters.ParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +/** + * Class to hold/create all parameters for test cases. + * + * @author Ajith Sreekumar (ajith.sreekumar@est.tech) + */ +public class CommonTestData { + + public static final String TOPIC_NAME = "policy-pdp-pap"; + public static final String TOPIC_INFRA = "kafka"; + public static final String TOPIC_SERVER = "kafka:9092"; + + public static final List TOPIC_PARAMS = + List.of(getTopicParameters(TOPIC_NAME, TOPIC_INFRA, TOPIC_SERVER)); + + protected static final Coder coder = new StandardCoder(); + + /** + * Create topic parameters for test cases. + * + * @param topicName name of topic + * @param topicInfra topicCommInfrastructure + * @param topicServer topic server + * @return topic parameters + */ + public static TopicParameters getTopicParameters(String topicName, String topicInfra, String topicServer) { + final TopicParameters topicParams = new TopicParameters(); + topicParams.setTopic(topicName); + topicParams.setTopicCommInfrastructure(topicInfra); + topicParams.setServers(List.of(topicServer)); + return topicParams; + } + + /** + * Converts the contents of a map to a parameter class. + * + * @param source property map + * @param clazz class of object to be created from the map + * @return a new object represented by the map + */ + public T toObject(final Map source, final Class clazz) { + try { + return coder.decode(coder.encode(source), clazz); + + } catch (final CoderException e) { + throw new RuntimeException("cannot create " + clazz.getName() + " from map", e); + } + } + + /** + * Returns a property map for a TopicParameters map for test cases. + * + * @param isEmpty boolean value to represent that object created should be empty or not + * @return a property map suitable for constructing an object + */ + public Map getTopicParameterGroupMap(final boolean isEmpty) { + final Map map = new TreeMap<>(); + if (!isEmpty) { + map.put("topicSources", TOPIC_PARAMS); + map.put("topicSinks", TOPIC_PARAMS); + } + + return map; + } + + /** + * Gets the standard parameter group as a String. + * + * @param filePath path of the file + * @return the standard parameters + * @throws IOException when file read operation fails + */ + public String getParameterGroupAsString(String filePath) throws IOException { + File file = new File(filePath); + return Files.readString(file.toPath()); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java new file mode 100644 index 000000000..6f0e38ddd --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java @@ -0,0 +1,400 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicFactories; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicPropertyBuilder; +import org.onap.policy.common.message.bus.event.noop.NoopTopicFactories; +import org.onap.policy.common.message.bus.event.noop.NoopTopicPropertyBuilder; +import org.onap.policy.common.message.bus.properties.MessageBusProperties; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class TopicEndpointProxyTest { + + private static final String NOOP_SOURCE_TOPIC = "noop-source"; + private static final String NOOP_SINK_TOPIC = "noop-sink"; + + private static final String KAFKA_SOURCE_TOPIC = "kafka-source"; + private static final String KAFKA_SINK_TOPIC = "kafka-sink"; + + private final Properties configuration = new Properties(); + private final TopicParameterGroup group = new TopicParameterGroup(); + + /** + * Constructor. + */ + public TopicEndpointProxyTest() { + group.setTopicSinks(new LinkedList<>()); + group.setTopicSources(new LinkedList<>()); + + NoopTopicPropertyBuilder noopSourceBuilder = + new NoopTopicPropertyBuilder(MessageBusProperties.PROPERTY_NOOP_SOURCE_TOPICS) + .makeTopic(NOOP_SOURCE_TOPIC); + configuration.putAll(noopSourceBuilder.build()); + group.getTopicSources().add(noopSourceBuilder.getParams()); + + NoopTopicPropertyBuilder noopSinkBuilder = + new NoopTopicPropertyBuilder(MessageBusProperties.PROPERTY_NOOP_SINK_TOPICS) + .makeTopic(NOOP_SINK_TOPIC); + configuration.putAll(noopSinkBuilder.build()); + group.getTopicSinks().add(noopSinkBuilder.getParams()); + + TopicParameters invalidCommInfraParams = + new NoopTopicPropertyBuilder(MessageBusProperties.PROPERTY_NOOP_SOURCE_TOPICS) + .makeTopic(NOOP_SOURCE_TOPIC).getParams(); + invalidCommInfraParams.setTopicCommInfrastructure(Topic.CommInfrastructure.REST.name()); + group.getTopicSources().add(invalidCommInfraParams); + group.getTopicSinks().add(invalidCommInfraParams); + } + + private boolean exists(List topics, String topicName) { + return topics.stream().map(Topic::getTopic).anyMatch(topicName::equals); + } + + private boolean allSources(List topics) { + return exists(topics, NOOP_SOURCE_TOPIC); + } + + private boolean allSinks(List topics) { + return exists(topics, NOOP_SINK_TOPIC); + } + + private boolean anySource(List topics) { + return exists(topics, NOOP_SOURCE_TOPIC); + } + + private boolean anySink(List topics) { + return exists(topics, NOOP_SINK_TOPIC); + } + + /** + * Destroys all managed topics. + */ + @AfterEach + public void tearDown() { + NoopTopicFactories.getSinkFactory().destroy(); + NoopTopicFactories.getSourceFactory().destroy(); + KafkaTopicFactories.getSinkFactory().destroy(); + KafkaTopicFactories.getSourceFactory().destroy(); + } + + @Test + void testSerialize() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + manager.addTopicSinks(configuration); + + assertThatCode(() -> new GsonTestUtils().compareGson(manager, TopicEndpointProxyTest.class)) + .doesNotThrowAnyException(); + } + + @Test + void testAddTopicSourcesListOfTopicParameters() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List sources = manager.addTopicSources(group.getTopicSources()); + assertSame(1, sources.size()); + + assertTrue(allSources(sources)); + assertFalse(anySink(sources)); + + sources = manager.addTopicSources(group.getTopicSources()); + assertSame(1, sources.size()); + assertTrue(allSources(sources)); + } + + @Test + void testAddTopicSourcesKafka() { + TopicEndpoint manager = new TopicEndpointProxy(); + + KafkaTopicPropertyBuilder kafkaTopicPropertyBuilder = + new KafkaTopicPropertyBuilder(MessageBusProperties.PROPERTY_KAFKA_SOURCE_TOPICS) + .makeTopic(KAFKA_SOURCE_TOPIC); + + configuration.putAll(kafkaTopicPropertyBuilder.build()); + group.getTopicSources().add(kafkaTopicPropertyBuilder.getParams()); + List sources = manager.addTopicSources(group.getTopicSources()); + assertSame(2, sources.size()); + + configuration.remove(KAFKA_SOURCE_TOPIC); + group.setTopicSources(new LinkedList<>()); + sources = manager.addTopicSources(group.getTopicSources()); + assertSame(0, sources.size()); + } + + @Test + void testAddTopicSourcesProperties() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List sources = manager.addTopicSources(configuration); + assertSame(1, sources.size()); + + assertTrue(allSources(sources)); + assertFalse(anySink(sources)); + } + + @Test + void testAddTopicSinksListOfTopicParameters() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List sinks = manager.addTopicSinks(group.getTopicSinks()); + assertSame(1, sinks.size()); + + assertFalse(anySource(sinks)); + assertTrue(allSinks(sinks)); + } + + @Test + void testAddTopicSinksListOfTopicParametersKafka() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List sinks = manager.addTopicSinks(group.getTopicSinks()); + assertSame(1, sinks.size()); + + KafkaTopicPropertyBuilder kafkaTopicPropertyBuilder = + new KafkaTopicPropertyBuilder(MessageBusProperties.PROPERTY_KAFKA_SINK_TOPICS) + .makeTopic(KAFKA_SINK_TOPIC); + + configuration.putAll(kafkaTopicPropertyBuilder.build()); + group.getTopicSources().add(kafkaTopicPropertyBuilder.getParams()); + sinks = manager.addTopicSinks(group.getTopicSources()); + assertSame(2, sinks.size()); + + configuration.remove(KAFKA_SOURCE_TOPIC); + group.setTopicSources(new LinkedList<>()); + sinks = manager.addTopicSinks(group.getTopicSources()); + assertSame(0, sinks.size()); + } + + @Test + void testAddTopicSinksProperties() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List sinks = manager.addTopicSinks(configuration); + assertSame(1, sinks.size()); + + assertFalse(anySource(sinks)); + assertTrue(allSinks(sinks)); + } + + @Test + void testAddTopicsProperties() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List topics = manager.addTopics(configuration); + assertSame(2, topics.size()); + + assertTrue(allSources(topics)); + assertTrue(allSinks(topics)); + } + + @Test + void testAddTopicsTopicParameterGroup() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List topics = manager.addTopics(group); + assertSame(2, topics.size()); + + assertTrue(allSources(topics)); + assertTrue(allSinks(topics)); + } + + @Test + void testAddTopicsTopicParameterGroupNull() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List topics = manager.addTopics(new TopicParameterGroup()); + assertEquals(0, topics.size()); + } + + @Test + void testLockSinks_lockSources_locked() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.lock(); + for (Topic topic : manager.addTopics(group)) { + assertTrue(topic.isLocked()); + } + } + + @Test + void testLockSinks_lockSources_unlocked() { + TopicEndpoint manager = new TopicEndpointProxy(); + for (Topic topic : manager.addTopics(group)) { + assertFalse(topic.isLocked()); + } + } + + @Test + void testGetTopicSources() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + manager.addTopicSinks(configuration); + + List sources = manager.getTopicSources(); + assertSame(1, sources.size()); + + assertTrue(allSources(sources)); + assertFalse(anySink(sources)); + + assertThatThrownBy(() -> manager.getKafkaTopicSource("testTopic")) + .hasMessageContaining("KafkaTopiceSource for testTopic not found"); + + List topicName = null; + assertThatThrownBy(() -> manager.getTopicSources(topicName)) + .hasMessageContaining("must provide a list of topics"); + } + + @Test + void testGetTopicSinks() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + manager.addTopicSinks(configuration); + + List sinks = manager.getTopicSinks(); + assertSame(1, sinks.size()); + + assertFalse(anySource(sinks)); + assertTrue(allSinks(sinks)); + + final List sinks2 = null; + assertThatThrownBy(() -> manager.getTopicSinks(sinks2)).hasMessageContaining("must provide a list of topics"); + + List sinks3 = List.of(NOOP_SINK_TOPIC); + assertThatCode(() -> manager.getTopicSinks(sinks3)).doesNotThrowAnyException(); + + String sinkTest = null; + assertThatThrownBy(() -> manager.getTopicSinks(sinkTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid parameter"); + + assertThatThrownBy(() -> manager.getKafkaTopicSink("testTopic")) + .hasMessageContaining("KafkaTopicSink for testTopic not found"); + } + + @Test + void testGetNoopTopicSources() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + assertSame(1, manager.getNoopTopicSources().size()); + } + + @Test + void testGetNoopTopicSinks() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSinks(configuration); + assertSame(1, manager.getNoopTopicSinks().size()); + } + + @Test + void testLifecycle() { + TopicEndpoint manager = new TopicEndpointProxy(); + + assertTrue(manager.start()); + assertTrue(manager.isAlive()); + + assertTrue(manager.stop()); + assertFalse(manager.isAlive()); + + assertTrue(manager.start()); + assertTrue(manager.isAlive()); + + manager.shutdown(); + assertFalse(manager.isAlive()); + } + + @Test + void testLock() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.lock(); + assertTrue(manager.isLocked()); + + manager.unlock(); + assertFalse(manager.isLocked()); + } + + @Test + void testGetTopicSource() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSources(configuration); + + assertSame(NOOP_SOURCE_TOPIC, manager.getTopicSource(CommInfrastructure.NOOP, NOOP_SOURCE_TOPIC).getTopic()); + + assertThatIllegalStateException() + .isThrownBy(() -> manager.getTopicSource(CommInfrastructure.NOOP, NOOP_SINK_TOPIC)); + } + + @Test + void testGetTopicSink() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSinks(configuration); + + assertSame(NOOP_SINK_TOPIC, manager.getTopicSink(CommInfrastructure.NOOP, NOOP_SINK_TOPIC).getTopic()); + + assertThatIllegalStateException() + .isThrownBy(() -> manager.getTopicSink(CommInfrastructure.NOOP, NOOP_SOURCE_TOPIC)); + } + + @Test + void testGetNoopTopicSource() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSources(configuration); + + assertSame(NOOP_SOURCE_TOPIC, manager.getNoopTopicSource(NOOP_SOURCE_TOPIC).getTopic()); + + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSource(null)); + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSource("")); + } + + @Test + void testGetNoopTopicSink() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSinks(configuration); + + assertSame(NOOP_SINK_TOPIC, manager.getNoopTopicSink(NOOP_SINK_TOPIC).getTopic()); + + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSink(null)); + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSink("")); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java new file mode 100644 index 000000000..db28892ef --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java @@ -0,0 +1,147 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019-2024 Nordix Foundation. + * Modifications Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +/** + * Class to perform unit test of {@link TopicParameterGroup}. + * + * @author Ajith Sreekumar (ajith.sreekumar@est.tech) + */ +class TopicParameterGroupTest { + private static final CommonTestData testData = new CommonTestData(); + private static final Coder coder = new StandardCoder(); + private final String packageDir = "src/test/resources/org/onap/policy/common/message/bus/parameters/"; + + @Test + void test() throws CoderException { + final TopicParameterGroup topicParameterGroup = + testData.toObject(testData.getTopicParameterGroupMap(false), TopicParameterGroup.class); + final ValidationResult validationResult = topicParameterGroup.validate(); + assertTrue(validationResult.isValid()); + assertEquals(CommonTestData.TOPIC_PARAMS, topicParameterGroup.getTopicSinks()); + assertEquals(CommonTestData.TOPIC_PARAMS, topicParameterGroup.getTopicSources()); + + // these should default to true + assertTrue(new TopicParameters().isManaged()); + assertTrue(coder.decode("{}", TopicParameters.class).isManaged()); + + // but can be overridden + assertFalse(coder.decode("{'managed':false}".replace('\'', '"'), TopicParameters.class).isManaged()); + } + + @Test + void testValidate() { + final TopicParameterGroup topicParameterGroup = + testData.toObject(testData.getTopicParameterGroupMap(false), TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertNull(result.getResult()); + assertTrue(result.isValid()); + } + + @Test + void test_valid() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_valid.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertNull(result.getResult()); + assertTrue(result.isValid()); + } + + @Test + void test_invalid() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_invalid.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertFalse(result.isValid()); + assertTrue(result.getResult().contains("INVALID")); + } + + @Test + void test_missing_mandatory_params() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_missing_mandatory.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertTrue(result.getResult().contains("Mandatory parameters are missing")); + assertFalse(result.isValid()); + } + + @Test + void test_allParams() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_all_params.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertNull(result.getResult()); + assertTrue(result.isValid()); + assertTrue(checkIfAllParamsNotEmpty(topicParameterGroup.getTopicSinks())); + assertTrue(checkIfAllParamsNotEmpty(topicParameterGroup.getTopicSources())); + } + + /** + * Method to check if all parameters in TopicParameters are set. + * Any parameters added to @link TopicParameters or @link BusTopicParams must be added to + * TopicParameters_all_params.json. + * + * @param topicParametersList list of topic parameters + * @return true if all parameters are not empty (if string) or true (if boolean) + * @throws Exception the exception + */ + private boolean checkIfAllParamsNotEmpty(List topicParametersList) throws Exception { + for (TopicParameters topicParameters : topicParametersList) { + Field[] fields = BusTopicParams.class.getDeclaredFields(); + for (Field field : fields) { + if (!field.isSynthetic() && !Modifier.isStatic(field.getModifiers())) { + Object parameter = new PropertyDescriptor(field.getName(), TopicParameters.class).getReadMethod() + .invoke(topicParameters); + if ((parameter instanceof String && StringUtils.isBlank(parameter.toString())) + || (parameter instanceof Boolean && !(Boolean) parameter) + || (parameter instanceof Number && ((Number) parameter).longValue() == 0)) { + return false; + } + } + } + } + return true; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java new file mode 100644 index 000000000..207023e58 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java @@ -0,0 +1,282 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.message.bus.event.base.BusConsumer.FetchingBusConsumer; +import org.onap.policy.common.message.bus.event.base.BusConsumer.KafkaConsumerWrapper; +import org.onap.policy.common.message.bus.properties.MessageBusProperties; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class BusConsumerTest extends TopicTestBase { + + private static final int SHORT_TIMEOUT_MILLIS = 10; + private static final int LONG_TIMEOUT_MILLIS = 3000; + + @Mock + KafkaConsumer mockedKafkaConsumer; + + AutoCloseable closeable; + + @BeforeEach + @Override + public void setUp() { + super.setUp(); + closeable = MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); + } + + + @Test + void testFetchingBusConsumer() { + // should not be negative + var cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(-1).build()); + assertThat(cons.getSleepTime()).isEqualTo(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH); + + // should not be zero + cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(0).build()); + assertThat(cons.getSleepTime()).isEqualTo(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH); + + // should not be too large + cons = new FetchingBusConsumerImpl( + makeBuilder().fetchTimeout(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH + 100).build()); + assertThat(cons.getSleepTime()).isEqualTo(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH); + + // should not be what was specified + cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(100).build()); + assertThat(cons.getSleepTime()).isEqualTo(100); + } + + @Test + void testFetchingBusConsumerSleepAfterFetchFailure() throws InterruptedException { + + var cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(SHORT_TIMEOUT_MILLIS).build()) { + + private CountDownLatch started = new CountDownLatch(1); + + @Override + protected void sleepAfterFetchFailure() { + started.countDown(); + super.sleepAfterFetchFailure(); + } + }; + + // full sleep + long tstart = System.currentTimeMillis(); + cons.sleepAfterFetchFailure(); + assertThat(System.currentTimeMillis() - tstart).isGreaterThanOrEqualTo(SHORT_TIMEOUT_MILLIS); + + // close while sleeping - sleep should halt prematurely + cons.fetchTimeout = LONG_TIMEOUT_MILLIS; + cons.started = new CountDownLatch(1); + Thread thread = new Thread(cons::sleepAfterFetchFailure); + tstart = System.currentTimeMillis(); + thread.start(); + cons.started.await(); + cons.close(); + thread.join(); + assertThat(System.currentTimeMillis() - tstart).isLessThan(LONG_TIMEOUT_MILLIS); + + // interrupt while sleeping - sleep should halt prematurely + cons.fetchTimeout = LONG_TIMEOUT_MILLIS; + cons.started = new CountDownLatch(1); + thread = new Thread(cons::sleepAfterFetchFailure); + tstart = System.currentTimeMillis(); + thread.start(); + cons.started.await(); + thread.interrupt(); + thread.join(); + assertThat(System.currentTimeMillis() - tstart).isLessThan(LONG_TIMEOUT_MILLIS); + } + + @Test + void testKafkaConsumerWrapper() { + // verify that different wrappers can be built + assertThatCode(() -> new KafkaConsumerWrapper(makeKafkaBuilder().build())).doesNotThrowAnyException(); + } + + @Test + void testKafkaConsumerWrapper_InvalidTopic() { + BusTopicParams params = makeBuilder().topic(null).build(); + assertThatThrownBy(() -> new KafkaConsumerWrapper(params)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testKafkaConsumerWrapperFetch() { + + //Setup Properties for consumer + Properties kafkaProps = new Properties(); + kafkaProps.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + kafkaProps.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "test"); + kafkaProps.setProperty("enable.auto.commit", "true"); + kafkaProps.setProperty("auto.commit.interval.ms", "1000"); + kafkaProps.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + kafkaProps.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + kafkaProps.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + kafkaProps.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + + KafkaConsumerWrapper kafka = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + KafkaConsumer consumer = new KafkaConsumer<>(kafkaProps); + kafka.consumer = consumer; + + assertThrows(java.lang.IllegalStateException.class, () -> kafka.fetch().iterator().hasNext()); + consumer.close(); + } + + @Test + void testFetchNoMessages() { + KafkaConsumerWrapper kafkaConsumerWrapper = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + kafkaConsumerWrapper.consumer = mockedKafkaConsumer; + + when(mockedKafkaConsumer.poll(any())).thenReturn(new ConsumerRecords<>(Collections.emptyMap())); + + Iterable result = kafkaConsumerWrapper.fetch(); + + verify(mockedKafkaConsumer).poll(any()); + + assertNotNull(result); + + assertFalse(result.iterator().hasNext()); + + mockedKafkaConsumer.close(); + } + + @Test + void testFetchWithMessages() { + // Setup + KafkaConsumerWrapper kafkaConsumerWrapper = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + kafkaConsumerWrapper.consumer = mockedKafkaConsumer; + + ConsumerRecord customerRecord = + new ConsumerRecord<>("my-effective-topic", 0, 0, "key", "value"); + Map>> recordsMap = new HashMap<>(); + recordsMap.put(new TopicPartition("my-effective-topic", 0), Collections.singletonList(customerRecord)); + ConsumerRecords consumerRecords = new ConsumerRecords<>(recordsMap); + + when(mockedKafkaConsumer.poll(any())).thenReturn(consumerRecords); + + Iterable result = kafkaConsumerWrapper.fetch(); + + verify(mockedKafkaConsumer, times(1)).poll(any()); + + verify(mockedKafkaConsumer, times(1)).commitSync(any(Map.class)); + + assertNotNull(result); + + assertTrue(result.iterator().hasNext()); + + assertEquals("value", result.iterator().next()); + + mockedKafkaConsumer.close(); + } + + @Test + void testFetchWithMessagesAndTraceParent() { + // Setup + KafkaConsumerWrapper kafkaConsumerWrapper = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + kafkaConsumerWrapper.consumer = mockedKafkaConsumer; + + ConsumerRecord customerRecord = + new ConsumerRecord<>("my-effective-topic", 0, 0, "key", "value"); + customerRecord.headers().add( + "traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".getBytes(StandardCharsets.UTF_8) + ); + + Map>> recordsMap = new HashMap<>(); + recordsMap.put(new TopicPartition("my-effective-topic", 0), Collections.singletonList(customerRecord)); + ConsumerRecords consumerRecords = new ConsumerRecords<>(recordsMap); + + when(mockedKafkaConsumer.poll(any())).thenReturn(consumerRecords); + + Iterable result = kafkaConsumerWrapper.fetch(); + + verify(mockedKafkaConsumer, times(1)).poll(any()); + + verify(mockedKafkaConsumer, times(1)).commitSync(any(Map.class)); + + assertNotNull(result); + + assertTrue(result.iterator().hasNext()); + + assertEquals("value", result.iterator().next()); + + mockedKafkaConsumer.close(); + } + + + @Test + void testKafkaConsumerWrapperClose() { + assertThatCode(() -> new KafkaConsumerWrapper(makeKafkaBuilder().build()).close()).doesNotThrowAnyException(); + } + + @Test + void testKafkaConsumerWrapperToString() { + assertNotNull(new KafkaConsumerWrapper(makeKafkaBuilder().build()) {}.toString()); + } + + private static class FetchingBusConsumerImpl extends FetchingBusConsumer { + + protected FetchingBusConsumerImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public Iterable fetch() { + return null; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java new file mode 100644 index 000000000..343a56a8c --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java @@ -0,0 +1,139 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class BusTopicBaseTest extends TopicTestBase { + + private BusTopicBaseImpl base; + + /** + * Initializes the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + base = new BusTopicBaseImpl(builder.build()); + } + + @Test + void testToString() { + assertNotNull(base.toString()); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(base, BusTopicBaseTest.class)).doesNotThrowAnyException(); + } + + @Test + void testGetApiKey() { + assertEquals(MY_API_KEY, base.getApiKey()); + } + + @Test + void testGetApiSecret() { + assertEquals(MY_API_SECRET, base.getApiSecret()); + } + + @Test + void testIsUseHttps() { + assertTrue(base.isUseHttps()); + assertFalse(new BusTopicBaseImpl(builder.useHttps(false).build()).isUseHttps()); + } + + @Test + void testIsAllowSelfSignedCerts() { + assertTrue(base.isAllowSelfSignedCerts()); + assertFalse(new BusTopicBaseImpl(builder.allowSelfSignedCerts(false).build()).isAllowSelfSignedCerts()); + } + + @Test + void testTopic() { + assertEquals(MY_TOPIC, base.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, base.getEffectiveTopic()); + assertNotEquals(base.getTopic(), base.getEffectiveTopic()); + } + + @Test + void testAnyNullOrEmpty() { + assertFalse(base.anyNullOrEmpty()); + assertFalse(base.anyNullOrEmpty("any-none-null", "any-none-null-B")); + + assertTrue(base.anyNullOrEmpty(null, "any-first-null")); + assertTrue(base.anyNullOrEmpty("any-middle-null", null, "any-middle-null-B")); + assertTrue(base.anyNullOrEmpty("any-last-null", null)); + assertTrue(base.anyNullOrEmpty("any-empty", "")); + } + + @Test + void testAllNullOrEmpty() { + assertTrue(base.allNullOrEmpty()); + assertTrue(base.allNullOrEmpty("")); + assertTrue(base.allNullOrEmpty(null, "")); + + assertFalse(base.allNullOrEmpty("all-ok-only-one")); + assertFalse(base.allNullOrEmpty("all-ok-one", "all-ok-two")); + assertFalse(base.allNullOrEmpty("all-ok-null", null)); + assertFalse(base.allNullOrEmpty("", "all-ok-empty")); + assertFalse(base.allNullOrEmpty("", "all-one-ok", null)); + } + + private static class BusTopicBaseImpl extends BusTopicBase { + + public BusTopicBaseImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public void shutdown() { + // do nothing + } + + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java new file mode 100644 index 000000000..bd531114e --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java @@ -0,0 +1,238 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_HTTP_HTTPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; + +import java.util.List; +import java.util.Properties; +import java.util.function.Predicate; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Base class for Topic Factory tests that use BusTopicParams. + * + * @param type of topic managed by the factory + */ +public abstract class BusTopicFactoryTestBase extends TopicFactoryTestBase { + + /** + * Builds a topic. + * + * @param params the parameters used to configure the topic + * @return a new topic + */ + protected abstract T buildTopic(BusTopicParams params); + + /** + * Builds a topic. + * + * @param servers list of servers + * @param topic the topic name + * @return a new topic + */ + protected abstract T buildTopic(List servers, String topic); + + /** + * Gets the parameters used to build the most recent topic. + * + * @return the most recent topic's parameters + */ + protected abstract BusTopicParams getLastParams(); + + /** + * Tests building a topic using BusTopicParams. + */ + public void testBuildBusTopicParams() { + initFactory(); + + // two unmanaged topics + T item = buildTopic(makeBuilder().managed(false).effectiveTopic(null).build()); + T item2 = buildTopic(makeBuilder().managed(false).topic(TOPIC2).build()); + assertNotNull(item); + assertNotNull(item2); + assertEquals(item.getTopic(), item.getEffectiveTopic()); + assertNotEquals(item2.getTopic(), item2.getEffectiveTopic()); + assertNotSame(item, item2); + + // duplicate topics, but since they aren't managed, they should be different + T item3 = buildTopic(makeBuilder().managed(false).build()); + T item4 = buildTopic(makeBuilder().managed(false).effectiveTopic(TOPIC2).build()); + assertNotNull(item3); + assertNotNull(item4); + assertEquals(MY_TOPIC, item4.getTopic()); + assertEquals(TOPIC2, item4.getEffectiveTopic()); + assertNotSame(item, item3); + assertNotSame(item, item4); + assertNotSame(item3, item4); + + // two managed topics + T item5 = buildTopic(makeBuilder().build()); + T item6 = buildTopic(makeBuilder().topic(TOPIC2).build()); + assertNotNull(item5); + assertNotNull(item6); + + // re-build same managed topics - should get exact same objects + assertSame(item5, buildTopic(makeBuilder().topic(MY_TOPIC).build())); + assertSame(item6, buildTopic(makeBuilder().topic(TOPIC2).build())); + } + + /** + * Tests exception cases when building a topic using BusTopicParams. + */ + public void testBuildBusTopicParams_Ex() { + // null topic + assertThatIllegalArgumentException().isThrownBy(() -> buildTopic(makeBuilder().topic(null).build())); + + // empty topic + assertThatIllegalArgumentException().isThrownBy(() -> buildTopic(makeBuilder().topic("").build())); + } + + /** + * Tests building a topic using a list of servers and a topic. + */ + public void testBuildListOfStringString() { + initFactory(); + + T item1 = buildTopic(servers, MY_TOPIC); + assertNotNull(item1); + + // check parameters that were used + BusTopicParams params = getLastParams(); + assertEquals(servers, params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertTrue(params.isManaged()); + assertFalse(params.isUseHttps()); + + T item2 = buildTopic(servers, TOPIC2); + assertNotNull(item2); + assertNotSame(item1, item2); + + // duplicate - should be the same, as these topics are managed + T item3 = buildTopic(servers, TOPIC2); + assertSame(item2, item3); + } + + /** + * Tests building a topic using Properties. Verifies parameters specific to Bus + * topics. + */ + public void testBuildProperties() { + initFactory(); + + List topics = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + assertEquals(1, topics.size()); + assertEquals(MY_TOPIC, topics.get(0).getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, topics.get(0).getEffectiveTopic()); + + BusTopicParams params = getLastParams(); + assertTrue(params.isManaged()); + assertTrue(params.isUseHttps()); + assertTrue(params.isAllowSelfSignedCerts()); + assertEquals(MY_API_KEY, params.getApiKey()); + assertEquals(MY_API_SECRET, params.getApiSecret()); + assertEquals(List.of(SERVER), params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + + List topics2 = buildTopics(makePropBuilder().makeTopic(TOPIC3) + .removeTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX).build()); + assertEquals(1, topics2.size()); + assertEquals(TOPIC3, topics2.get(0).getTopic()); + assertEquals(topics2.get(0).getTopic(), topics2.get(0).getEffectiveTopic()); + } + + @Override + void testBuildProperties_Variations() { + super.testBuildProperties_Variations(); + + // check boolean properties that default to true + checkDefault(PROPERTY_MANAGED_SUFFIX, BusTopicParams::isManaged); + + // check boolean properties that default to false + checkDefault(PROPERTY_HTTP_HTTPS_SUFFIX, params -> !params.isUseHttps()); + checkDefault(PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX, params -> !params.isAllowSelfSignedCerts()); + } + + /** + * Verifies that a parameter has the correct default, if the original builder property + * is not provided. + * + * @param builderName name of the builder property + * @param validate function to test the validity of the property + * @param values the values to which the property should be set, defaults to + * {@code null} and "" + */ + protected void checkDefault(String builderName, Predicate validate, Object... values) { + Object[] values2 = (values.length > 0 ? values : new Object[] {null, ""}); + + for (Object value : values2) { + // always start with a fresh factory + initFactory(); + + TopicPropertyBuilder builder = makePropBuilder().makeTopic(MY_TOPIC); + + if (value == null) { + builder.removeTopicProperty(builderName); + + } else { + builder.setTopicProperty(builderName, value.toString()); + } + + assertEquals(1, buildTopics(builder.build()).size(), "size for default " + value); + assertTrue(validate.test(getLastParams()), "default for " + value); + } + } + + /** + * Verifies that an "additional" property does not exist, if the original builder + * property is not provided. + * + * @param builderName name of the builder property + * @param addName name of the "additional" property + */ + public void expectNullAddProp(String builderName, String addName) { + + // remove the property + initFactory(); + Properties props = makePropBuilder().makeTopic(MY_TOPIC).removeTopicProperty(builderName).build(); + assertEquals(1, buildTopics(props).size()); + assertFalse(getLastParams().getAdditionalProps().containsKey(addName)); + + + // repeat, this time using an empty string instead of null + initFactory(); + props = makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(builderName, "").build(); + assertEquals(1, buildTopics(props).size()); + assertFalse(getLastParams().getAdditionalProps().containsKey(addName)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java new file mode 100644 index 000000000..820fc2c31 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java @@ -0,0 +1,230 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class InlineBusTopicSinkTest extends TopicTestBase { + + private InlineBusTopicSinkImpl sink; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + sink = new InlineBusTopicSinkImpl(makeBuilder().build()); + } + + @AfterEach + public void tearDown() { + sink.shutdown(); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(sink, InlineBusTopicSinkTest.class)) + .doesNotThrowAnyException(); + } + + @Test + void testInlineBusTopicSinkImpl() { + // verify that different wrappers can be built + sink = new InlineBusTopicSinkImpl(makeBuilder().build()); + assertEquals(MY_PARTITION, sink.getPartitionKey()); + + sink = new InlineBusTopicSinkImpl(makeBuilder().partitionId(null).build()); + assertNotNull(sink.getPartitionKey()); + } + + @Test + void testStart() { + assertTrue(sink.start()); + assertEquals(1, sink.initCount); + + // re-start, init() should not be invoked again + assertTrue(sink.start()); + assertEquals(1, sink.initCount); + } + + @Test + void testStart_Locked() { + sink.lock(); + assertThatThrownBy(() -> sink.start()).isInstanceOf(IllegalStateException.class); + } + + @Test + void testStop() { + BusPublisher pub = mock(BusPublisher.class); + sink.publisher = pub; + + assertTrue(sink.stop()); + verify(pub).close(); + + // stop again, shouldn't not invoke close() again + assertFalse(sink.stop()); + verify(pub).close(); + + // publisher throws exception + sink = new InlineBusTopicSinkImpl(makeBuilder().build()); + sink.publisher = pub; + doThrow(new RuntimeException(EXPECTED)).when(pub).close(); + assertTrue(sink.stop()); + } + + @Test + void testSend() { + sink.start(); + BusPublisher pub = mock(BusPublisher.class); + sink.publisher = pub; + + TopicListener listener = mock(TopicListener.class); + sink.register(listener); + + assertTrue(sink.send(MY_MESSAGE)); + + verify(pub).send(MY_PARTITION, MY_MESSAGE); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + assertEquals(List.of(MY_MESSAGE), Arrays.asList(sink.getRecentEvents())); + + // arrange for send to throw an exception + when(pub.send(anyString(), anyString())).thenThrow(new RuntimeException(EXPECTED)); + + assertFalse(sink.send(MY_MESSAGE)); + + // no more event deliveries + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + } + + @Test + void testSend_NullMessage() { + sink.start(); + sink.publisher = mock(BusPublisher.class); + + assertThatThrownBy(() -> sink.send(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testSend_EmptyMessage() { + sink.start(); + sink.publisher = mock(BusPublisher.class); + + assertThatThrownBy(() -> sink.send("")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testSend_NotStarted() { + sink.publisher = mock(BusPublisher.class); + assertThatThrownBy(() -> sink.send(MY_MESSAGE)).isInstanceOf(IllegalStateException.class); + } + + @Test + void testSetPartitionKey_getPartitionKey() { + assertEquals(MY_PARTITION, sink.getPartitionKey()); + + sink.setPartitionKey("part-B"); + assertEquals("part-B", sink.getPartitionKey()); + } + + @Test + void testShutdown() { + BusPublisher pub = mock(BusPublisher.class); + sink.publisher = pub; + + sink.shutdown(); + verify(pub).close(); + } + + @Test + void testAnyNullOrEmpty() { + assertFalse(sink.anyNullOrEmpty()); + assertFalse(sink.anyNullOrEmpty("any-none-null", "any-none-null-B")); + + assertTrue(sink.anyNullOrEmpty(null, "any-first-null")); + assertTrue(sink.anyNullOrEmpty("any-middle-null", null, "any-middle-null-B")); + assertTrue(sink.anyNullOrEmpty("any-last-null", null)); + assertTrue(sink.anyNullOrEmpty("any-empty", "")); + } + + @Test + void testAllNullOrEmpty() { + assertTrue(sink.allNullOrEmpty()); + assertTrue(sink.allNullOrEmpty("")); + assertTrue(sink.allNullOrEmpty(null, "")); + + assertFalse(sink.allNullOrEmpty("all-ok-only-one")); + assertFalse(sink.allNullOrEmpty("all-ok-one", "all-ok-two")); + assertFalse(sink.allNullOrEmpty("all-ok-null", null)); + assertFalse(sink.allNullOrEmpty("", "all-ok-empty")); + assertFalse(sink.allNullOrEmpty("", "all-one-ok", null)); + } + + @Test + void testToString() { + assertTrue(sink.toString().startsWith("InlineBusTopicSink [")); + } + + /** + * Implementation of InlineBusTopicSink that tracks the number of times that init() is + * invoked. + */ + private static class InlineBusTopicSinkImpl extends InlineBusTopicSink { + + private int initCount = 0; + + public InlineBusTopicSinkImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public void init() { + ++initCount; + } + + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java new file mode 100644 index 000000000..8ad8e8fdb --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java @@ -0,0 +1,375 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.gson.GsonTestUtils; +import org.onap.policy.common.utils.network.NetworkUtil; + +class SingleThreadedBusTopicSourceTest extends TopicTestBase { + private Thread thread; + private BusConsumer cons; + private TopicListener listener; + private SingleThreadedBusTopicSourceImpl source; + + /** + * Creates the object to be tested, as well as various mocks. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + thread = mock(Thread.class); + cons = mock(BusConsumer.class); + listener = mock(TopicListener.class); + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + } + + @AfterEach + public void tearDown() { + source.shutdown(); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(source, SingleThreadedBusTopicSourceTest.class)) + .doesNotThrowAnyException(); + } + + @Test + void testRegister() { + source.register(listener); + assertEquals(1, source.initCount); + source.offer(MY_MESSAGE); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + + // register another - should not re-init + TopicListener listener2 = mock(TopicListener.class); + source.register(listener2); + assertEquals(1, source.initCount); + source.offer(MY_MESSAGE + "z"); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE + "z"); + verify(listener2).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE + "z"); + + // re-register - should not re-init + source.register(listener); + assertEquals(1, source.initCount); + source.offer(MY_MESSAGE2); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE2); + + // lock & register - should not init + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + source.lock(); + source.register(listener); + assertEquals(0, source.initCount); + + // exception during init + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + source.initEx = true; + source.register(listener); + } + + @Test + void testUnregister() { + TopicListener listener2 = mock(TopicListener.class); + source.register(listener); + source.register(listener2); + + // unregister first listener - should NOT invoke close + source.unregister(listener); + verify(cons, never()).close(); + assertEquals(Arrays.asList(listener2), source.snapshotTopicListeners()); + + // unregister same listener - should not invoke close + source.unregister(listener); + verify(cons, never()).close(); + assertEquals(Arrays.asList(listener2), source.snapshotTopicListeners()); + + // unregister second listener - SHOULD invoke close + source.unregister(listener2); + verify(cons).close(); + assertTrue(source.snapshotTopicListeners().isEmpty()); + + // unregister same listener - should not invoke close again + source.unregister(listener2); + verify(cons).close(); + assertTrue(source.snapshotTopicListeners().isEmpty()); + } + + @Test + void testToString() { + assertTrue(source.toString().startsWith("SingleThreadedBusTopicSource [")); + } + + @Test + void testMakePollerThread() { + SingleThreadedBusTopicSource source2 = new SingleThreadedBusTopicSource(makeBuilder().build()) { + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public void init() throws MalformedURLException { + // do nothing + } + }; + + assertNotNull(source2.makePollerThread()); + } + + @Test + void testSingleThreadedBusTopicSource() { + // Note: if the value contains "-", it's probably a UUID + + // verify that different wrappers can be built + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + assertThat(source.getConsumerGroup()).isEqualTo(MY_CONS_GROUP); + assertThat(source.getConsumerInstance()).isEqualTo(MY_CONS_INST); + + // group is null => group is UUID, instance is as provided + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().consumerGroup(null).build()); + assertThat(source.getConsumerGroup()).contains("-").isNotEqualTo(NetworkUtil.getHostname()); + assertThat(source.getConsumerInstance()).isEqualTo(MY_CONS_INST); + + // instance is null => group is as provided, instance is UUID + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().consumerInstance(null).build()); + assertThat(source.getConsumerGroup()).isEqualTo(MY_CONS_GROUP); + assertThat(source.getConsumerInstance()).contains("-").isNotEqualTo(NetworkUtil.getHostname()); + + // group & instance are null => group is UUID, instance is hostname + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().consumerGroup(null).consumerInstance(null).build()); + assertThat(source.getConsumerGroup()).contains("-").isNotEqualTo(NetworkUtil.getHostname()); + assertThat(source.getConsumerInstance()).isEqualTo(NetworkUtil.getHostname()); + + assertThatCode(() -> new SingleThreadedBusTopicSourceImpl( + makeBuilder().fetchLimit(-1).fetchTimeout(-1).build())).doesNotThrowAnyException(); + } + + @Test + void testStart() { + source.start(); + assertTrue(source.isAlive()); + assertEquals(1, source.initCount); + verify(thread).start(); + + // attempt to start again - nothing should be invoked again + source.start(); + assertTrue(source.isAlive()); + assertEquals(1, source.initCount); + verify(thread).start(); + + // stop & re-start + source.stop(); + source.start(); + assertTrue(source.isAlive()); + assertEquals(2, source.initCount); + verify(thread, times(2)).start(); + } + + @Test + void testStart_Locked() { + source.lock(); + assertThatThrownBy(() -> source.start()).isInstanceOf(IllegalStateException.class); + } + + @Test + void testStart_InitEx() { + assertThatThrownBy(() -> { + source.initEx = true; + + source.start(); + }).isInstanceOf(IllegalStateException.class); + } + + @Test + void testStop() { + source.start(); + source.stop(); + verify(cons).close(); + + // stop it again - not re-closed + source.stop(); + verify(cons).close(); + + // start & stop again, but with an exception + doThrow(new RuntimeException(EXPECTED)).when(cons).close(); + source.start(); + source.stop(); + } + + @Test + void testRun() throws Exception { + source.register(listener); + + /* + * Die in the middle of fetching messages. Also, throw an exception during the + * first fetch attempt. + */ + when(cons.fetch()).thenAnswer(new Answer>() { + int count = 0; + + @Override + public Iterable answer(InvocationOnMock invocation) throws Throwable { + if (++count > 1) { + source.alive = false; + return Arrays.asList(MY_MESSAGE, MY_MESSAGE2); + + } else { + throw new IOException(EXPECTED); + } + } + }); + source.alive = true; + source.run(); + assertEquals(Arrays.asList(MY_MESSAGE), Arrays.asList(source.getRecentEvents())); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + verify(listener, never()).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE2); + + /* + * Die AFTER fetching messages. + */ + final String msga = "message-A"; + final String msgb = "message-B"; + when(cons.fetch()).thenAnswer(new Answer>() { + int count = 0; + + @Override + public Iterable answer(InvocationOnMock invocation) throws Throwable { + if (++count > 1) { + source.alive = false; + return Collections.emptyList(); + + } else { + return Arrays.asList(msga, msgb); + } + } + }); + source.alive = true; + source.run(); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msga); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msgb); + + assertEquals(Arrays.asList(MY_MESSAGE, msga, msgb), Arrays.asList(source.getRecentEvents())); + } + + @Test + void testOffer() { + source.register(listener); + source.offer(MY_MESSAGE); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + assertEquals(Arrays.asList(MY_MESSAGE), Arrays.asList(source.getRecentEvents())); + } + + @Test + void testOffer_NotStarted() { + assertThatThrownBy(() -> source.offer(MY_MESSAGE)).isInstanceOf(IllegalStateException.class); + } + + @Test + void testGetConsumerGroup() { + assertEquals(MY_CONS_GROUP, source.getConsumerGroup()); + } + + @Test + void testGetConsumerInstance() { + assertEquals(MY_CONS_INST, source.getConsumerInstance()); + } + + @Test + void testShutdown() { + source.register(listener); + + source.shutdown(); + verify(cons).close(); + assertTrue(source.snapshotTopicListeners().isEmpty()); + } + + @Test + void testGetFetchTimeout() { + assertEquals(MY_FETCH_TIMEOUT, source.getFetchTimeout()); + } + + @Test + void testGetFetchLimit() { + assertEquals(MY_FETCH_LIMIT, source.getFetchLimit()); + } + + /** + * Implementation of SingleThreadedBusTopicSource that counts the number of times + * init() is invoked. + */ + private class SingleThreadedBusTopicSourceImpl extends SingleThreadedBusTopicSource { + + private int initCount = 0; + private boolean initEx = false; + + public SingleThreadedBusTopicSourceImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public void init() throws MalformedURLException { + ++initCount; + + if (initEx) { + throw new MalformedURLException(EXPECTED); + } + + consumer = cons; + } + + @Override + protected Thread makePollerThread() { + return thread; + } + + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java new file mode 100644 index 000000000..5ecde2585 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java @@ -0,0 +1,355 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class TopicBaseTest extends TopicTestBase { + + private TopicBaseImpl base; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + base = new TopicBaseImpl(servers, MY_TOPIC); + } + + @Test + void testTopicBase_NullServers() { + assertThatThrownBy(() -> new TopicBaseImpl(null, MY_TOPIC)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_EmptyServers() { + List testList = Collections.emptyList(); + assertThatThrownBy(() -> new TopicBaseImpl(testList, MY_TOPIC)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_NullTopic() { + assertThatThrownBy(() -> new TopicBaseImpl(servers, null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_EmptyTopic() { + assertThatThrownBy(() -> new TopicBaseImpl(servers, "")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_EffectiveTopic() { + TopicBase baseEf = new TopicBaseImpl(servers, MY_TOPIC, MY_EFFECTIVE_TOPIC); + assertEquals(MY_TOPIC, baseEf.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, baseEf.getEffectiveTopic()); + } + + @Test + void testTopicBase_NullEffectiveTopic() { + TopicBase baseEf = new TopicBaseImpl(servers, MY_TOPIC, null); + assertEquals(MY_TOPIC, baseEf.getTopic()); + assertEquals(MY_TOPIC, baseEf.getEffectiveTopic()); + } + + @Test + void testTopicBase_EmptyEffectiveTopic() { + TopicBase baseEf = new TopicBaseImpl(servers, MY_TOPIC, ""); + assertEquals(MY_TOPIC, baseEf.getTopic()); + assertEquals(MY_TOPIC, baseEf.getEffectiveTopic()); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(base, TopicBaseTest.class)).doesNotThrowAnyException(); + } + + @Test + void testRegister() { + TopicListener listener = mock(TopicListener.class); + base.register(listener); + assertEquals(List.of(listener), base.snapshotTopicListeners()); + + // re-register - list should be unchanged + base.register(listener); + assertEquals(List.of(listener), base.snapshotTopicListeners()); + + // register a new listener + TopicListener listener2 = mock(TopicListener.class); + base.register(listener2); + assertEquals(List.of(listener, listener2), base.snapshotTopicListeners()); + } + + @Test + void testRegister_NullListener() { + assertThatThrownBy(() -> base.register(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testUnregister() { + // register two listeners + TopicListener listener = mock(TopicListener.class); + TopicListener listener2 = mock(TopicListener.class); + base.register(listener); + base.register(listener2); + + // unregister one + base.unregister(listener); + assertEquals(List.of(listener2), base.snapshotTopicListeners()); + + // unregister the other + base.unregister(listener2); + assertTrue(base.snapshotTopicListeners().isEmpty()); + + // unregister again + base.unregister(listener2); + assertTrue(base.snapshotTopicListeners().isEmpty()); + } + + @Test + void testUnregister_NullListener() { + base.register(mock(TopicListener.class)); + assertThatThrownBy(() -> base.unregister(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testBroadcast() { + // register two listeners + TopicListener listener = mock(TopicListener.class); + TopicListener listener2 = mock(TopicListener.class); + base.register(listener); + base.register(listener2); + + // broadcast a message + final String msg1 = "message-A"; + base.broadcast(msg1); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg1); + verify(listener2).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg1); + + // broadcast another message, with an exception + final String msg2 = "message-B"; + doThrow(new RuntimeException(EXPECTED)).when(listener).onTopicEvent(any(), any(), any()); + base.broadcast(msg2); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg2); + verify(listener2).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg2); + } + + @Test + void testLock_testUnlock() { + assertFalse(base.isLocked()); + assertTrue(base.lock()); + assertEquals(0, base.startCount); + assertEquals(1, base.stopCount); + + // lock again - should not stop again + assertTrue(base.isLocked()); + assertTrue(base.lock()); + assertEquals(0, base.startCount); + assertEquals(1, base.stopCount); + + assertTrue(base.isLocked()); + assertTrue(base.unlock()); + assertEquals(1, base.startCount); + assertEquals(1, base.stopCount); + + // unlock again - should not start again + assertFalse(base.isLocked()); + assertTrue(base.unlock()); + assertEquals(1, base.startCount); + assertEquals(1, base.stopCount); + } + + /** + * Tests lock/unlock when the stop/start methods return false. + */ + @Test + void testLock_testUnlock_FalseReturns() { + + // lock, but stop returns false + base.stopReturn = false; + assertFalse(base.lock()); + assertTrue(base.isLocked()); + assertTrue(base.lock()); + + // unlock, but start returns false + base.startReturn = false; + assertFalse(base.unlock()); + assertFalse(base.isLocked()); + assertTrue(base.unlock()); + } + + /** + * Tests lock/unlock when the start method throws an exception. + */ + @Test + void testLock_testUnlock_Exception() { + + // lock & re-lock, but start throws an exception + base.startEx = true; + assertTrue(base.lock()); + assertFalse(base.unlock()); + assertFalse(base.isLocked()); + assertTrue(base.unlock()); + } + + @Test + void testIsLocked() { + assertFalse(base.isLocked()); + base.lock(); + assertTrue(base.isLocked()); + base.unlock(); + assertFalse(base.isLocked()); + } + + @Test + void testGetTopic() { + assertEquals(MY_TOPIC, base.getTopic()); + } + + @Test + void testGetEffectiveTopic() { + assertEquals(MY_TOPIC, base.getTopic()); + assertEquals(MY_TOPIC, base.getEffectiveTopic()); + } + + @Test + void testIsAlive() { + assertFalse(base.isAlive()); + base.start(); + assertTrue(base.isAlive()); + base.stop(); + assertFalse(base.isAlive()); + } + + @Test + void testGetServers() { + assertEquals(servers, base.getServers()); + } + + @Test + void testGetRecentEvents() { + assertEquals(0, base.getRecentEvents().length); + + base.addEvent("recent-A"); + base.addEvent("recent-B"); + + String[] recent = base.getRecentEvents(); + assertEquals(2, recent.length); + assertEquals("recent-A", recent[0]); + assertEquals("recent-B", recent[1]); + } + + @Test + void testToString() { + assertNotNull(base.toString()); + } + + /** + * Implementation of TopicBase. + */ + private static class TopicBaseImpl extends TopicBase { + private int startCount = 0; + private int stopCount = 0; + private boolean startReturn = true; + private boolean stopReturn = true; + private boolean startEx = false; + + /** + * Constructor. + * + * @param servers list of servers + * @param topic topic name + */ + public TopicBaseImpl(List servers, String topic) { + super(servers, topic); + } + + /** + * Constructor. + * + * @param servers list of servers + * @param topic topic name + * @param effectiveTopic effective topic name for network communication + */ + public TopicBaseImpl(List servers, String topic, String effectiveTopic) { + super(servers, topic, effectiveTopic); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public boolean start() { + ++startCount; + + if (startEx) { + throw new RuntimeException(EXPECTED); + } + + alive = true; + return startReturn; + } + + @Override + public boolean stop() { + ++stopCount; + alive = false; + return stopReturn; + } + + @Override + public void shutdown() { + // do nothing + } + + /** + * Adds an event to the list of recent events. + * + * @param event event to be added + */ + public void addEvent(String event) { + recentEvents.add(event); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java new file mode 100644 index 000000000..8444b482f --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java @@ -0,0 +1,225 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic; + +/** + * Base class for XxxTopicFactory tests. + * + * @param type of topic managed by the factory + */ +public abstract class TopicFactoryTestBase extends TopicTestBase { + + public static final String SERVER = "my-server"; + public static final String TOPIC2 = "my-topic-2"; + public static final String TOPIC3 = "my-topic-3"; + + /** + * Initializes a new factory. + */ + protected abstract void initFactory(); + + /** + * Makes a property builder. + * + * @return a new property builder + */ + protected abstract TopicPropertyBuilder makePropBuilder(); + + /** + * Builds a set of topics. + * + * @param properties the properties used to configure the topics + * @return a list of new topics + */ + protected abstract List buildTopics(Properties properties); + + /** + * Destroys the factory. + */ + protected abstract void destroyFactory(); + + /** + * Destroys a topic within the factory. + * + * @param topic the topic to destroy + */ + protected abstract void destroyTopic(String topic); + + /** + * Gets the list of topics from the factory. + * + * @return the topic inventory + */ + protected abstract List getInventory(); + + /** + * Gets a topic from the factory. + * + * @param topic the topic name + * @return the topic + */ + protected abstract T getTopic(String topic); + + + /** + * Tests building a topic using varied Properties. + */ + void testBuildProperties_Variations() { + initFactory(); + + // null topic list + assertTrue(buildTopics(makePropBuilder().build()).isEmpty()); + + // empty topic list + assertTrue(buildTopics(makePropBuilder().addTopic("").build()).isEmpty()); + + // null servers + assertTrue(buildTopics(makePropBuilder().makeTopic(MY_TOPIC).removeTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX) + .build()).isEmpty()); + + // empty servers + assertTrue(buildTopics(makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, "") + .build()).isEmpty()); + } + + /** + * Tests building multiple topics using Properties. + */ + public void testBuildProperties_Multiple() { + initFactory(); + + // make two fully-defined topics, and add two duplicate topic names to the list + TopicPropertyBuilder builder = + makePropBuilder().makeTopic(MY_TOPIC).makeTopic(TOPIC2).addTopic(MY_TOPIC).addTopic(MY_TOPIC); + + List lst = buildTopics(builder.build()); + assertEquals(4, lst.size()); + + int index = 0; + T item = lst.get(index++); + assertNotSame(item, lst.get(index++)); + assertSame(item, lst.get(index++)); + assertSame(item, lst.get(index++)); + } + + /** + * Tests destroy(topic), get(topic), and inventory() methods. + */ + public void testDestroyString_testGet_testInventory() { + initFactory(); + + List lst = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).makeTopic(TOPIC2).build()); + + int index = 0; + T item1 = lst.get(index++); + T item2 = lst.get(index++); + + assertEquals(2, getInventory().size()); + assertTrue(getInventory().contains(item1)); + assertTrue(getInventory().contains(item2)); + + item1.start(); + item2.start(); + + assertEquals(item1, getTopic(MY_TOPIC)); + assertEquals(item2, getTopic(TOPIC2)); + + destroyTopic(MY_TOPIC); + assertFalse(item1.isAlive()); + assertTrue(item2.isAlive()); + assertEquals(item2, getTopic(TOPIC2)); + assertEquals(1, getInventory().size()); + assertTrue(getInventory().contains(item2)); + + // repeat + destroyTopic(MY_TOPIC); + assertFalse(item1.isAlive()); + assertTrue(item2.isAlive()); + + // with other topic + destroyTopic(TOPIC2); + assertFalse(item1.isAlive()); + assertFalse(item2.isAlive()); + assertEquals(0, getInventory().size()); + } + + /** + * Tests exception cases with destroy(topic). + */ + public void testDestroyString_Ex() { + // null topic + assertThatIllegalArgumentException().as("null topic").isThrownBy(() -> destroyTopic(null)); + + // empty topic + assertThatIllegalArgumentException().as("empty topic").isThrownBy(() -> destroyTopic("")); + } + + /** + * Tests the destroy() method. + */ + public void testDestroy() { + initFactory(); + + List lst = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).makeTopic(TOPIC2).build()); + + int index = 0; + T item1 = lst.get(index++); + T item2 = lst.get(index++); + + item1.start(); + item2.start(); + + destroyFactory(); + + assertFalse(item1.isAlive()); + assertFalse(item2.isAlive()); + assertEquals(0, getInventory().size()); + } + + /** + * Tests exception cases with get(topic). + */ + public void testGet_Ex() { + // null topic + assertThatIllegalArgumentException().as("null topic").isThrownBy(() -> getTopic(null)); + + // empty topic + assertThatIllegalArgumentException().as("empty topic").isThrownBy(() -> getTopic("")); + + // unknown topic + initFactory(); + buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + + assertThatIllegalStateException().as("unknown topic").isThrownBy(() -> getTopic(TOPIC2)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java new file mode 100644 index 000000000..29c5306b0 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.Properties; + +/** + * Builder of properties used when configuring topics. + */ +public abstract class TopicPropertyBuilder { + private final Properties properties = new Properties(); + private final String prefix; + private String topicPrefix; + + /** + * Constructs the object. + * + * @param prefix the prefix for the properties to be built + */ + public TopicPropertyBuilder(String prefix) { + this.prefix = prefix; + } + + /** + * Constructs the properties from the builder. + * + * @return a copy of the properties + */ + public Properties build() { + Properties props = new Properties(); + props.putAll(properties); + + return props; + } + + /** + * Adds a topic to the list of topics, configuring all of its properties with default + * values. + * + * @param topic the topic to be added + * @return this builder + */ + public abstract TopicPropertyBuilder makeTopic(String topic); + + /** + * Adds a topic to the list of topics. Also sets the current topic so that subsequent + * invocations of property methods will manipulate the topic's properties. + * + * @param topic the topic to be added + * @return this builder + */ + public TopicPropertyBuilder addTopic(String topic) { + // add topic to the list of topics + String topicList = properties.getProperty(prefix); + if (topicList == null || topicList.isEmpty()) { + topicList = topic; + } else { + topicList += "," + topic; + } + + properties.setProperty(prefix, topicList); + + setTopic(topic); + + return this; + } + + /** + * Sets the topic for which subsequent properties will be managed. + * + * @param topic the topic + * @return this builder + */ + public TopicPropertyBuilder setTopic(String topic) { + this.topicPrefix = prefix + "." + topic; + return this; + } + + /** + * Sets a topic's property. + * + * @param name name of the property + * @param value value to which the property should be set + * @return this builder + */ + public TopicPropertyBuilder setTopicProperty(String name, Object value) { + properties.setProperty(topicPrefix + name, value.toString()); + return this; + } + + /** + * Removes a topic's property. + * + * @param name name of the property + * @return this builder + */ + public TopicPropertyBuilder removeTopicProperty(String name) { + properties.remove(topicPrefix + name); + return this; + } +} + diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java new file mode 100644 index 000000000..8d5c35359 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java @@ -0,0 +1,159 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.parameters.topic.BusTopicParams.TopicParamsBuilder; + +/** + * Base class for Topic Test classes. + */ +public class TopicTestBase { + + public static final String MY_AFT_ENV = "my-aft-env"; + public static final String MY_API_KEY = "my-api-key"; + public static final String MY_API_SECRET = "my-api-secret"; + public static final String MY_BASE_PATH = "my-base"; + public static final String MY_CLIENT_NAME = "my-client"; + public static final String MY_CONS_GROUP = "my-cons-group"; + public static final String MY_CONS_INST = "my-cons-inst"; + public static final String MY_ENV = "my-env"; + public static final int MY_FETCH_LIMIT = 100; + public static final int MY_FETCH_TIMEOUT = 101; + public static final String MY_HOST = "my-host"; + public static final String MY_LAT = "my-lat"; + public static final String MY_LONG = "my-long"; + public static final String MY_PARTNER = "my-partner"; + public static final String MY_PASS = "my-pass"; + public static final int MY_PORT = 102; + public static final String MY_TOPIC = "my-topic"; + public static final String MY_EFFECTIVE_TOPIC = "my-effective-topic"; + public static final String MY_USERNAME = "my-user"; + + public static final String MY_MESSAGE = "my-message"; + public static final String MY_PARTITION = "my-partition"; + public static final String MY_MESSAGE2 = "my-message-2"; + + public static final String MY_SERIALIZER = "org.apache.kafka.common.serialization.StringSerializer"; + public static final int KAFKA_PORT = 9092; + + /** + * Message used within exceptions that are expected. + */ + public static final String EXPECTED = "expected exception"; + + /** + * Additional properties to be added to the parameter builder. + */ + protected Map addProps; + + /** + * Servers to be added to the parameter builder. + */ + protected List servers; + + /** + * Servers to be added to the parameter builder. + */ + protected List kafkaServers; + + /** + * Parameter builder used to build topic parameters. + */ + protected TopicParamsBuilder builder; + + /** + * Initializes {@link #addProps}, {@link #servers}, and {@link #builder}. + */ + public void setUp() { + addProps = new TreeMap<>(); + addProps.put("my-key-A", "my-value-A"); + addProps.put("my-key-B", "my-value-B"); + + servers = Arrays.asList("svra", "svrb"); + kafkaServers = Arrays.asList("localhost:9092", "10.1.2.3:9092"); + + builder = makeBuilder(); + } + + /** + * Makes a fully populated parameter builder. + * + * @return a new parameter builder + */ + public TopicParamsBuilder makeBuilder() { + return makeBuilder(addProps, servers); + } + + /** + * Makes a fully populated parameter builder. + * + * @param addProps additional properties to be added to the builder + * @param servers servers to be added to the builder + * @return a new parameter builder + */ + public TopicParamsBuilder makeBuilder(Map addProps, List servers) { + + return BusTopicParams.builder().additionalProps(addProps).aftEnvironment(MY_AFT_ENV).allowSelfSignedCerts(true) + .apiKey(MY_API_KEY).apiSecret(MY_API_SECRET).basePath(MY_BASE_PATH).clientName(MY_CLIENT_NAME) + .consumerGroup(MY_CONS_GROUP).consumerInstance(MY_CONS_INST).environment(MY_ENV) + .fetchLimit(MY_FETCH_LIMIT).fetchTimeout(MY_FETCH_TIMEOUT).hostname(MY_HOST).latitude(MY_LAT) + .longitude(MY_LONG).managed(true).partitionId(MY_PARTITION).partner(MY_PARTNER) + .password(MY_PASS).port(MY_PORT).servers(servers).topic(MY_TOPIC) + .effectiveTopic(MY_EFFECTIVE_TOPIC).useHttps(true).allowTracing(true).userName(MY_USERNAME) + .serializationProvider(MY_SERIALIZER); + } + + /** + * Makes a fully populated parameter builder. + * + * @return a new parameter builder + */ + public TopicParamsBuilder makeKafkaBuilder() { + addProps.clear(); + String jaas = "org.apache.kafka.common.security.plain.PlainLoginModule " + + "required username=abc password=abc serviceName=kafka;"; + addProps.put("sasl.jaas.config", jaas); + addProps.put("sasl.mechanism", "SCRAM-SHA-512"); + addProps.put("security.protocol", "SASL_PLAINTEXT"); + + return makeKafkaBuilder(addProps, kafkaServers); + } + + /** + * Makes a fully populated parameter builder. + * + * @param addProps additional properties to be added to the builder + * @param servers servers to be added to the builder + * @return a new parameter builder + */ + public TopicParamsBuilder makeKafkaBuilder(Map addProps, List servers) { + + return BusTopicParams.builder().additionalProps(addProps).basePath(MY_BASE_PATH).clientName(MY_CLIENT_NAME) + .consumerGroup(MY_CONS_GROUP).consumerInstance(MY_CONS_INST).environment(MY_ENV) + .hostname(MY_HOST).partitionId(MY_PARTITION).partner(MY_PARTNER).fetchTimeout(MY_FETCH_TIMEOUT) + .port(KAFKA_PORT).servers(servers).topic(MY_TOPIC) + .effectiveTopic(MY_EFFECTIVE_TOPIC).useHttps(false).allowTracing(true); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java new file mode 100644 index 000000000..93341a9ee --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java @@ -0,0 +1,35 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; + +class TopicClientExceptionTest { + + @Test + void test() { + assertEquals(5, new ExceptionsTester().test(TopicSinkClientException.class)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java new file mode 100644 index 000000000..67b15ec95 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java @@ -0,0 +1,147 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.event.TopicSink; + +class TopicSinkClientTest { + private static final String TOPIC = "my-topic"; + + private TopicSinkClient client; + private TopicSink sink; + private List sinks; + + /** + * Creates mocks and an initial client object. + * + * @throws Exception if an error occurs + */ + @BeforeEach + public void setUp() throws Exception { + sink = mock(TopicSink.class); + when(sink.send(anyString())).thenReturn(true); + + sinks = Arrays.asList(sink, null); + + client = new TopicSinkClient2(TOPIC); + + Properties props = new Properties(); + props.setProperty("noop.sink.topics", TOPIC); + + // clear all topics and then configure one topic + TopicEndpointManager.getManager().shutdown(); + TopicEndpointManager.getManager().addTopicSinks(props); + } + + @AfterAll + public static void tearDown() { + // clear all topics after the tests + TopicEndpointManager.getManager().shutdown(); + } + + /** + * Uses a real NO-OP topic sink. + */ + @Test + void testGetTopicSinks() throws Exception { + + sink = TopicEndpointManager.getManager().getNoopTopicSink(TOPIC); + assertNotNull(sink); + + final AtomicReference evref = new AtomicReference<>(null); + + sink.register((infra, topic, event) -> evref.set(event)); + sink.start(); + + client = new TopicSinkClient(TOPIC); + client.send(100); + + assertEquals("100", evref.get()); + } + + @Test + void testTopicSinkClient() { + // unknown topic -> should throw exception + sinks = new LinkedList<>(); + assertThatThrownBy(() -> new TopicSinkClient2(TOPIC)).isInstanceOf(TopicSinkClientException.class) + .hasMessage("no sinks for topic: my-topic"); + } + + @Test + void testTopicSinkClient_GetTopic() throws TopicSinkClientException { + assertEquals(TOPIC, new TopicSinkClient(TopicEndpointManager.getManager().getNoopTopicSink(TOPIC)).getTopic()); + assertEquals(TOPIC, new TopicSinkClient(TOPIC).getTopic()); + + assertThatThrownBy(() -> new TopicSinkClient((TopicSink) null)) + .hasMessageContaining("sink is marked non-null but is null"); + assertThatThrownBy(() -> new TopicSinkClient("blah")).isInstanceOf(TopicSinkClientException.class) + .hasMessage("no sinks for topic: blah"); + } + + @Test + void testSend() { + client.send(Arrays.asList("abc", "def")); + verify(sink).send("['abc','def']".replace('\'', '"')); + + // sink send fails + when(sink.send(anyString())).thenReturn(false); + assertFalse(client.send("ghi")); + + // sink send throws an exception + final RuntimeException ex = new RuntimeException("expected exception"); + when(sink.send(anyString())).thenThrow(ex); + assertFalse(client.send("jkl")); + } + + /** + * TopicSinkClient with some overrides. + */ + private class TopicSinkClient2 extends TopicSinkClient { + + public TopicSinkClient2(final String topic) throws TopicSinkClientException { + super(topic); + } + + @Override + protected List getTopicSinks(final String topic) { + return sinks; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java new file mode 100644 index 000000000..a901b07bd --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java @@ -0,0 +1,63 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class IndexedKafkaTopicSourceFactoryTest { + + private IndexedKafkaTopicSourceFactory factory; + + @Test + void testBuild() { + factory = new IndexedKafkaTopicSourceFactory(); + BusTopicParams params = new BusTopicParams(); + + // set servers to null + params.setServers(null); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("KAFKA Server(s) must be provided"); + + // set servers to empty + params.setServers(List.of()); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("KAFKA Server(s) must be provided"); + + List servers = List.of("kafka:9092", "kafka:29092"); + params.setServers(servers); + + // set topic to null + params.setTopic(null); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("A topic must be provided"); + + // set topic to empty + params.setTopic(""); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("A topic must be provided"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java new file mode 100644 index 000000000..c8d6e21e3 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java @@ -0,0 +1,68 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; + +class InlineKafkaTopicSinkTest extends TopicTestBase { + private InlineKafkaTopicSink sink; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + sink = new InlineKafkaTopicSink(makeKafkaBuilder().build()); + } + + @AfterEach + public void tearDown() { + sink.shutdown(); + } + + @Test + void testToString() { + assertTrue(sink.toString().startsWith("InlineKafkaTopicSink [")); + } + + @Test + void testInit() { + // nothing null + sink = new InlineKafkaTopicSink(makeKafkaBuilder().build()); + sink.init(); + assertThatCode(() -> sink.shutdown()).doesNotThrowAnyException(); + } + + @Test + void testGetTopicCommInfrastructure() { + assertEquals(CommInfrastructure.KAFKA, sink.getTopicCommInfrastructure()); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java new file mode 100644 index 000000000..8e13af239 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Properties; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class KafkaPublisherWrapperTest { + + private KafkaPublisherWrapper kafkaPublisherWrapper; + private Producer mockProducer; + private BusTopicParams mockBusTopicParams; + + @BeforeEach + void setUp() { + mockProducer = mock(KafkaProducer.class); + mockBusTopicParams = mock(BusTopicParams.class); + + when(mockBusTopicParams.getTopic()).thenReturn("testTopic"); + when(mockBusTopicParams.getServers()).thenReturn(Collections.singletonList("localhost:9092")); + when(mockBusTopicParams.isTopicInvalid()).thenReturn(false); + when(mockBusTopicParams.isAdditionalPropsValid()).thenReturn(false); + when(mockBusTopicParams.isAllowTracing()).thenReturn(false); + + kafkaPublisherWrapper = new KafkaPublisherWrapper(mockBusTopicParams) { + private Producer createProducer(Properties props) { // NOSONAR instance creation + return mockProducer; + } + }; + } + + @Test + void testConstructor() { + verify(mockBusTopicParams).getTopic(); + verify(mockBusTopicParams).getServers(); + verify(mockBusTopicParams).isTopicInvalid(); + verify(mockBusTopicParams).isAdditionalPropsValid(); + verify(mockBusTopicParams).isAllowTracing(); + } + + @Test + void testSendSuccess() { + when(mockProducer.send(ArgumentMatchers.any(ProducerRecord.class))).thenReturn(null); + assertTrue(kafkaPublisherWrapper.send("partitionId", "testMessage")); + } + + @Test + void testSendNullMessage() { + IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> kafkaPublisherWrapper.send("partitionId", null), + "Expected send() to throw, but it didn't" + ); + assertEquals("No message provided", thrown.getMessage()); + } + + @Test + void testSendFailure() { + when(mockProducer.send(ArgumentMatchers.any(ProducerRecord.class))).thenThrow(RuntimeException.class); + assertTrue(kafkaPublisherWrapper.send("partitionId", "testMessage")); + } + + @Test + void testClose() { + assertThatCode(kafkaPublisherWrapper::close).doesNotThrowAnyException(); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java new file mode 100644 index 000000000..1085ee905 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import java.util.Collections; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.base.BusTopicFactoryTestBase; + +/** + * Base class for KafkaTopicXxxFactory tests. + * + * @param type of topic managed by the factory + */ +public abstract class KafkaTopicFactoryTestBase extends BusTopicFactoryTestBase { + + @Override + public void testBuildBusTopicParams_Ex() { + + super.testBuildBusTopicParams_Ex(); + + // null servers + assertThatIllegalArgumentException().as("null servers") + .isThrownBy(() -> buildTopic(makeBuilder().servers(null).build())); + + // empty servers + assertThatIllegalArgumentException().as("empty servers") + .isThrownBy(() -> buildTopic(makeBuilder().servers(Collections.emptyList()).build())); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java new file mode 100644 index 000000000..6a1be7a07 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java @@ -0,0 +1,94 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.onap.policy.common.message.bus.event.base.TopicTestBase.MY_EFFECTIVE_TOPIC; +import static org.onap.policy.common.message.bus.event.base.TopicTestBase.MY_PARTITION; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_HTTP_HTTPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.TopicParameters; + +@Getter +public class KafkaTopicPropertyBuilder extends TopicPropertyBuilder { + + public static final String SERVER = "localhost:9092"; + public static final String TOPIC2 = "my-topic-2"; + public static final String ADDITIONAL_PROPS = "{\"security.protocol\": \"SASL_PLAINTEXT\"," + + "\"sasl.mechanism\": \"SCRAM-SHA-512\",\"sasl.jaas.config\": " + + "\"org.apache.kafka.common.security.plain.PlainLoginModule " + + "required username=abc password=abc serviceName=kafka;\"}"; + + private final TopicParameters params = new TopicParameters(); + + /** + * Constructs the object. + * + * @param prefix the prefix for the properties to be built + */ + public KafkaTopicPropertyBuilder(String prefix) { + super(prefix); + } + + /** + * Adds a topic and configures it's properties with default values. + * + * @param topic the topic to be added + * @return this builder + */ + public KafkaTopicPropertyBuilder makeTopic(String topic) { + addTopic(topic); + + setTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX, MY_EFFECTIVE_TOPIC); + setTopicProperty(PROPERTY_MANAGED_SUFFIX, "true"); + setTopicProperty(PROPERTY_HTTP_HTTPS_SUFFIX, "true"); + setTopicProperty(PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX, MY_PARTITION); + setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, SERVER); + setTopicProperty(".additionalProps", ADDITIONAL_PROPS); + + params.setTopicCommInfrastructure("kafka"); + params.setTopic(topic); + params.setEffectiveTopic(MY_EFFECTIVE_TOPIC); + params.setManaged(true); + params.setUseHttps(true); + params.setPartitionId(MY_PARTITION); + params.setServers(List.of(SERVER)); + params.setAdditionalProps(getAdditionalProps()); + + return this; + } + + private Map getAdditionalProps() { + try { + return new ObjectMapper().readValue(ADDITIONAL_PROPS, Map.class); + } catch (JsonProcessingException e) { + return Collections.emptyMap(); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java new file mode 100644 index 000000000..62210361c --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java @@ -0,0 +1,200 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SINK_TOPICS; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class KafkaTopicSinkFactoryTest extends KafkaTopicFactoryTestBase { + + private SinkFactory factory; + public static final String KAFKA_SERVER = "localhost:9092"; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + factory = new SinkFactory(); + } + + @AfterEach + public void tearDown() { + factory.destroy(); + } + + @Test + @Override + public void testBuildBusTopicParams() { + super.testBuildBusTopicParams(); + super.testBuildBusTopicParams_Ex(); + } + + @Test + @Override + public void testBuildListOfStringString() { + super.testBuildListOfStringString(); + + // check parameters that were used + BusTopicParams params = getLastParams(); + assertFalse(params.isAllowSelfSignedCerts()); + } + + @Test + @Override + public void testBuildProperties() { + List topics = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + assertEquals(1, topics.size()); + assertEquals(MY_TOPIC, topics.get(0).getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, topics.get(0).getEffectiveTopic()); + + BusTopicParams params = getLastParams(); + assertTrue(params.isManaged()); + assertFalse(params.isUseHttps()); + assertEquals(List.of(KAFKA_SERVER), params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + assertEquals(MY_PARTITION, params.getPartitionId()); + assertNotNull(params.getAdditionalProps()); + + List topics2 = buildTopics(makePropBuilder().makeTopic(TOPIC3) + .removeTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX).build()); + assertEquals(1, topics2.size()); + assertEquals(TOPIC3, topics2.get(0).getTopic()); + assertEquals(topics2.get(0).getTopic(), topics2.get(0).getEffectiveTopic()); + + initFactory(); + + assertEquals(1, buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()).size()); + } + + @Test + void testBuildFromProperties() { + Properties props = makePropBuilder().makeTopic(MY_TOPIC).build(); + var listTopic = factory.build(props); + assertNotNull(listTopic); + } + + @Test + @Override + public void testDestroyString_testGet_testInventory() { + super.testDestroyString_testGet_testInventory(); + super.testDestroyString_Ex(); + } + + @Test + @Override + public void testDestroy() { + super.testDestroy(); + } + + @Test + void testGet() { + super.testGet_Ex(); + } + + @Test + void testToString() { + assertTrue(factory.toString().startsWith("IndexedKafkaTopicSinkFactory [")); + } + + @Override + protected void initFactory() { + if (factory != null) { + factory.destroy(); + } + + factory = new SinkFactory(); + } + + @Override + protected List buildTopics(Properties properties) { + return factory.build(properties); + } + + @Override + protected KafkaTopicSink buildTopic(BusTopicParams params) { + return factory.build(params); + } + + @Override + protected KafkaTopicSink buildTopic(List servers, String topic) { + return factory.build(servers, topic); + } + + @Override + protected void destroyFactory() { + factory.destroy(); + } + + @Override + protected void destroyTopic(String topic) { + factory.destroy(topic); + } + + @Override + protected List getInventory() { + return factory.inventory(); + } + + @Override + protected KafkaTopicSink getTopic(String topic) { + return factory.get(topic); + } + + @Override + protected BusTopicParams getLastParams() { + return factory.params.getLast(); + } + + @Override + protected TopicPropertyBuilder makePropBuilder() { + return new KafkaTopicPropertyBuilder(PROPERTY_KAFKA_SINK_TOPICS); + } + + /** + * Factory that records the parameters of all the sinks it creates. + */ + private static class SinkFactory extends IndexedKafkaTopicSinkFactory { + private Deque params = new LinkedList<>(); + + @Override + protected KafkaTopicSink makeSink(BusTopicParams busTopicParams) { + params.add(busTopicParams); + return super.makeSink(busTopicParams); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java new file mode 100644 index 000000000..8818a27f4 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class KafkaTopicSinkTest { + + @Test + void test() { + assertNotNull(KafkaTopicFactories.getSinkFactory()); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java new file mode 100644 index 000000000..c91e548b5 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java @@ -0,0 +1,168 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SOURCE_TOPICS; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class KafkaTopicSourceFactoryTest extends KafkaTopicFactoryTestBase { + + private SourceFactory factory; + + public static final String KAFKA_SERVER = "localhost:9092"; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + factory = new SourceFactory(); + } + + @AfterEach + public void tearDown() { + factory.destroy(); + } + + @Test + @Override + public void testBuildProperties() { + + initFactory(); + + List topics = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + assertEquals(1, topics.size()); + assertEquals(MY_TOPIC, topics.get(0).getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, topics.get(0).getEffectiveTopic()); + + BusTopicParams params = getLastParams(); + assertTrue(params.isManaged()); + assertFalse(params.isUseHttps()); + assertEquals(List.of(KAFKA_SERVER), params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + } + + @Test + @Override + public void testDestroyString_testGet_testInventory() { + super.testDestroyString_testGet_testInventory(); + super.testDestroyString_Ex(); + } + + @Test + @Override + public void testDestroy() { + super.testDestroy(); + } + + @Test + void testGet() { + super.testGet_Ex(); + } + + @Test + void testToString() { + assertTrue(factory.toString().startsWith("IndexedKafkaTopicSourceFactory [")); + } + + @Override + protected void initFactory() { + if (factory != null) { + factory.destroy(); + } + + factory = new SourceFactory(); + } + + @Override + protected List buildTopics(Properties properties) { + return factory.build(properties); + } + + @Override + protected KafkaTopicSource buildTopic(BusTopicParams params) { + return factory.build(params); + } + + @Override + protected KafkaTopicSource buildTopic(List servers, String topic) { + return factory.build(servers, topic); + } + + @Override + protected void destroyFactory() { + factory.destroy(); + } + + @Override + protected void destroyTopic(String topic) { + factory.destroy(topic); + } + + @Override + protected List getInventory() { + return factory.inventory(); + } + + @Override + protected KafkaTopicSource getTopic(String topic) { + return factory.get(topic); + } + + @Override + protected BusTopicParams getLastParams() { + return factory.params.getLast(); + } + + @Override + protected TopicPropertyBuilder makePropBuilder() { + return new KafkaTopicPropertyBuilder(PROPERTY_KAFKA_SOURCE_TOPICS); + } + + /** + * Factory that records the parameters of all the sources it creates. + */ + private static class SourceFactory extends IndexedKafkaTopicSourceFactory { + private final Deque params = new LinkedList<>(); + + @Override + protected KafkaTopicSource makeSource(BusTopicParams busTopicParams) { + params.add(busTopicParams); + return super.makeSource(busTopicParams); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java new file mode 100644 index 000000000..5572bc017 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class KafkaTopicSourceTest { + + @Test + void verifyKafkaTopicFactoriesNotNull() { + assertNotNull(KafkaTopicFactories.getSourceFactory()); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java new file mode 100644 index 000000000..0c732ba78 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java @@ -0,0 +1,61 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; + +class SingleThreadedKafkaTopicSourceTest extends TopicTestBase { + private SingleThreadedKafkaTopicSource source; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + source = new SingleThreadedKafkaTopicSource(makeKafkaBuilder().build()); + } + + @AfterEach + public void tearDown() { + source.shutdown(); + } + + @Test + void testToString() { + assertTrue(source.toString().startsWith("SingleThreadedKafkaTopicSource [")); + source.shutdown(); + } + + @Test + void testGetTopicCommInfrastructure() { + assertEquals(CommInfrastructure.KAFKA, source.getTopicCommInfrastructure()); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java new file mode 100644 index 000000000..3ebec83cf --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; + +public abstract class NoopTopicEndpointTest, T extends NoopTopicEndpoint> + extends TopicTestBase { + + protected final F factory; + protected T endpoint; + + public NoopTopicEndpointTest(F factory) { + this.factory = factory; + } + + protected abstract boolean io(String message); + + @BeforeEach + @Override + public void setUp() { + super.setUp(); + this.endpoint = this.factory.build(servers, MY_TOPIC); + } + + @Test + void testIo() { + TopicListener listener = mock(TopicListener.class); + this.endpoint.register(listener); + this.endpoint.start(); + + assertTrue(io(MY_MESSAGE)); + assertSame(MY_MESSAGE, this.endpoint.getRecentEvents()[0]); + assertEquals(Collections.singletonList(MY_MESSAGE), Arrays.asList(this.endpoint.getRecentEvents())); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + + this.endpoint.unregister(listener); + } + + @Test + void testIoNullMessage() { + assertThatThrownBy(() -> io(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testIoEmptyMessage() { + assertThatThrownBy(() -> io("")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testOfferNotStarted() { + assertThatThrownBy(() -> io(MY_MESSAGE)).isInstanceOf(IllegalStateException.class); + } + + @Test + void testGetTopicCommInfrastructure() { + assertEquals(CommInfrastructure.NOOP, this.endpoint.getTopicCommInfrastructure()); + } + + @Test + void testStart_testStop_testShutdown() { + this.endpoint.start(); + assertTrue(this.endpoint.isAlive()); + + // start again + this.endpoint.start(); + assertTrue(this.endpoint.isAlive()); + + // stop + this.endpoint.stop(); + assertFalse(this.endpoint.isAlive()); + + // re-start again + this.endpoint.start(); + assertTrue(this.endpoint.isAlive()); + + // shutdown + this.endpoint.shutdown(); + assertFalse(this.endpoint.isAlive()); + } + + @Test + void testStart_Locked() { + this.endpoint.lock(); + assertThatThrownBy(() -> this.endpoint.start()).isInstanceOf(IllegalStateException.class); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java new file mode 100644 index 000000000..4dcba86e7 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java @@ -0,0 +1,257 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicFactoryTestBase; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +public abstract class NoopTopicFactoryTest, T extends NoopTopicEndpoint> + extends TopicFactoryTestBase { + + private static final List NOOP_SERVERS = List.of(CommInfrastructure.NOOP.toString()); + private F factory = null; + + protected abstract F buildFactory(); + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + initFactory(); + } + + @AfterEach + void tearDown() { + factory.destroy(); + } + + @Test + void testBuildBusTopicParams() { + initFactory(); + + T item1 = buildTopic(makeParams(servers)); + assertNotNull(item1); + + assertEquals(servers, item1.getServers()); + assertEquals(MY_TOPIC, item1.getTopic()); + } + + @Test + void testBuildListOfStringStringBoolean() { + initFactory(); + + T item1 = buildTopic(servers, MY_TOPIC, true); + assertNotNull(item1); + + assertEquals(servers, item1.getServers()); + assertEquals(MY_TOPIC, item1.getTopic()); + + // managed topic - should not build a new one + assertEquals(item1, buildTopic(servers, MY_TOPIC, true)); + + T item2 = buildTopic(servers, TOPIC2, true); + assertNotNull(item2); + assertNotSame(item1, item2); + + // duplicate - should be the same, as these topics are managed + List randomServers = new ArrayList<>(); + randomServers.add(RandomStringUtils.randomAlphanumeric(8)); + T item3 = buildTopic(randomServers, TOPIC2, true); + assertSame(item2, item3); + + T item4 = buildTopic(Collections.emptyList(), TOPIC2, true); + assertSame(item3, item4); + + // null server list + initFactory(); + assertEquals(NOOP_SERVERS, buildTopic(null, MY_TOPIC, true).getServers()); + + // empty server list + initFactory(); + assertEquals(NOOP_SERVERS, buildTopic(Collections.emptyList(), MY_TOPIC, true).getServers()); + + // unmanaged topic + initFactory(); + item1 = buildTopic(servers, MY_TOPIC, false); + assertNotSame(item1, buildTopic(servers, MY_TOPIC, false)); + } + + @Test + void testBuildListOfStringStringBoolean_NullTopic() { + assertThatThrownBy(() -> buildTopic(servers, null, true)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testBuildListOfStringStringBoolean_EmptyTopic() { + assertThatThrownBy(() -> buildTopic(servers, "", true)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testBuildProperties() { + // managed topic + initFactory(); + assertEquals(1, buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()).size()); + assertNotNull(factory.get(MY_TOPIC)); + + // unmanaged topic - get() will throw an exception + initFactory(); + assertEquals(1, buildTopics(makePropBuilder().makeTopic(MY_TOPIC) + .setTopicProperty(PROPERTY_MANAGED_SUFFIX, "false").build()).size()); + assertThatIllegalStateException().isThrownBy(() -> factory.get(MY_TOPIC)); + + // managed undefined - default to true + initFactory(); + assertEquals(1, buildTopics( + makePropBuilder().makeTopic(MY_TOPIC).removeTopicProperty(PROPERTY_MANAGED_SUFFIX).build()) + .size()); + assertNotNull(factory.get(MY_TOPIC)); + + // managed empty - default to true + initFactory(); + assertEquals(1, buildTopics( + makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(PROPERTY_MANAGED_SUFFIX, "").build()) + .size()); + assertNotNull(factory.get(MY_TOPIC)); + + initFactory(); + + // null topic list + assertTrue(buildTopics(makePropBuilder().build()).isEmpty()); + + // empty topic list + assertTrue(buildTopics(makePropBuilder().addTopic("").build()).isEmpty()); + + // null server list + initFactory(); + T endpoint = buildTopics(makePropBuilder().makeTopic(MY_TOPIC) + .removeTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX).build()).get(0); + assertEquals(NOOP_SERVERS, endpoint.getServers()); + + // empty server list + initFactory(); + endpoint = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, "") + .build()).get(0); + assertEquals(NOOP_SERVERS, endpoint.getServers()); + + // test other options + super.testBuildProperties_Multiple(); + } + + @Test + @Override + public void testDestroyString_testGet_testInventory() { + super.testDestroyString_testGet_testInventory(); + super.testDestroyString_Ex(); + } + + @Test + @Override + public void testDestroy() { + super.testDestroy(); + } + + @Test + void testGet() { + super.testGet_Ex(); + } + + @Override + protected void initFactory() { + if (factory != null) { + factory.destroy(); + } + + factory = buildFactory(); + } + + @Override + protected List buildTopics(Properties properties) { + return factory.build(properties); + } + + protected T buildTopic(BusTopicParams param) { + return factory.build(param); + } + + protected T buildTopic(List servers, String topic, boolean managed) { + return factory.build(servers, topic, managed); + } + + @Override + protected void destroyFactory() { + factory.destroy(); + } + + @Override + protected void destroyTopic(String topic) { + factory.destroy(topic); + } + + @Override + protected List getInventory() { + return factory.inventory(); + } + + @Override + protected T getTopic(String topic) { + return factory.get(topic); + } + + @Override + protected TopicPropertyBuilder makePropBuilder() { + return new NoopTopicPropertyBuilder(factory.getTopicsPropertyName()); + } + + private BusTopicParams makeParams(List servers) { + BusTopicParams params = new BusTopicParams(); + + params.setServers(servers); + params.setTopic(TopicTestBase.MY_TOPIC); + params.setManaged(true); + + return params; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java new file mode 100644 index 000000000..57b784356 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.event.base.TopicTestBase.MY_EFFECTIVE_TOPIC; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_HTTP_HTTPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import java.util.List; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.TopicParameters; + +@Getter +public class NoopTopicPropertyBuilder extends TopicPropertyBuilder { + + public static final String SERVER = "my-server"; + + private final TopicParameters params = new TopicParameters(); + + /** + * Constructs the object. + * + * @param prefix the prefix for the properties to be built + */ + public NoopTopicPropertyBuilder(String prefix) { + super(prefix); + } + + /** + * Adds a topic and configures it's properties with default values. + * + * @param topic the topic to be added + * @return this builder + */ + public NoopTopicPropertyBuilder makeTopic(String topic) { + addTopic(topic); + + setTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX, MY_EFFECTIVE_TOPIC); + setTopicProperty(PROPERTY_MANAGED_SUFFIX, "true"); + setTopicProperty(PROPERTY_HTTP_HTTPS_SUFFIX, "true"); + setTopicProperty(PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX, "true"); + setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, SERVER); + + params.setTopicCommInfrastructure("noop"); + params.setTopic(topic); + params.setEffectiveTopic(MY_EFFECTIVE_TOPIC); + params.setManaged(true); + params.setUseHttps(true); + params.setAllowSelfSignedCerts(true); + params.setServers(List.of(SERVER)); + + return this; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java new file mode 100644 index 000000000..ecd4df1e5 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NoopTopicSinkFactoryTest extends NoopTopicFactoryTest { + + @Override + protected NoopTopicSinkFactory buildFactory() { + return new NoopTopicSinkFactory(); + } + + @Test + void testToString() { + assertTrue(new NoopTopicSinkFactory().toString().startsWith("NoopTopicSinkFactory [")); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java new file mode 100644 index 000000000..ecba379f5 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +class NoopTopicSinkTest extends NoopTopicEndpointTest { + + public NoopTopicSinkTest() { + super(new NoopTopicSinkFactory()); + } + + @Override + protected boolean io(String message) { + return endpoint.send(message); + } + + @Test + void testToString() { + assertThat(endpoint.toString()).startsWith("NoopTopicSink"); + } + + @Test + void testSend() { + NoopTopicSink sink = new NoopTopicSink(servers, MY_TOPIC) { + @Override + protected boolean broadcast(String message) { + throw new RuntimeException(EXPECTED); + } + + }; + + sink.start(); + assertFalse(sink.send(MY_MESSAGE)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java new file mode 100644 index 000000000..c90380680 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NoopTopicSourceFactoryTest extends NoopTopicFactoryTest { + + @Override + protected NoopTopicSourceFactory buildFactory() { + return new NoopTopicSourceFactory(); + } + + @Test + void testToString() { + assertTrue(new NoopTopicSourceFactory().toString().startsWith("NoopTopicSourceFactory [")); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java new file mode 100644 index 000000000..51ff109bf --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NoopTopicSourceTest extends NoopTopicEndpointTest { + + public NoopTopicSourceTest() { + super(new NoopTopicSourceFactory()); + } + + @Override + protected boolean io(String message) { + return this.endpoint.offer(message); + } + + @Test + void testToString() { + assertTrue(this.endpoint.toString().startsWith("NoopTopicSource")); + } + + @Test + void testOffer() { + NoopTopicSource source = new NoopTopicSource(servers, MY_TOPIC) { + @Override + protected boolean broadcast(String message) { + throw new RuntimeException(EXPECTED); + } + + }; + + source.start(); + assertFalse(source.offer(MY_MESSAGE)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java new file mode 100644 index 000000000..776c5d57a --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java @@ -0,0 +1,86 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.features; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.slf4j.Logger; + +@ExtendWith(MockitoExtension.class) +class NetLoggerFeatureApiTest { + + @Mock + private Logger mockLogger; + + @Mock + private EventType mockEventType; + + @Mock + private CommInfrastructure mockCommInfrastructure; + + private NetLoggerFeatureApi featureApi; + + @BeforeEach + public void setUp() { + featureApi = new NetLoggerFeatureApi() { + @Override + public boolean beforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return NetLoggerFeatureApi.super.beforeLog(eventLogger, type, protocol, topic, message); + } + + @Override + public boolean afterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return NetLoggerFeatureApi.super.afterLog(eventLogger, type, protocol, topic, message); + } + + @Override + public int getSequenceNumber() { + return 0; + } + + @Override + public String getName() { + return NetLoggerFeatureApi.super.getName(); + } + }; + } + + @Test + void testBeforeLogDefaultBehavior() { + boolean result = featureApi.beforeLog(mockLogger, mockEventType, mockCommInfrastructure, + "testTopic", "testMessage"); + assertFalse(result, "Expected beforeLog to return false by default"); + } + + @Test + void testAfterLogDefaultBehavior() { + boolean result = featureApi.afterLog(mockLogger, mockEventType, mockCommInfrastructure, + "testTopic", "testMessage"); + assertFalse(result, "Expected afterLog to return false by default"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactoryTest.java new file mode 100644 index 000000000..b71731f2f --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactoryTest.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.TopicParameters; + +class TopicHealthCheckFactoryTest { + + @Test + void testGetTopicHealthCheck() { + var topicHealthCheckFactory = new TopicHealthCheckFactory(); + var param = new TopicParameters(); + param.setTopicCommInfrastructure(Topic.CommInfrastructure.NOOP.name()); + var topicHealthCheck = topicHealthCheckFactory.getTopicHealthCheck(param); + assertNotNull(topicHealthCheck); + param.setTopicCommInfrastructure(Topic.CommInfrastructure.KAFKA.name()); + topicHealthCheck = topicHealthCheckFactory.getTopicHealthCheck(param); + assertNotNull(topicHealthCheck); + param.setTopicCommInfrastructure(Topic.CommInfrastructure.REST.name()); + topicHealthCheck = topicHealthCheckFactory.getTopicHealthCheck(param); + assertNull(topicHealthCheck); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheckTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheckTest.java new file mode 100644 index 000000000..3b65f73c7 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheckTest.java @@ -0,0 +1,122 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck.kafka; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.DescribeClusterResult; +import org.apache.kafka.clients.admin.ListTopicsResult; +import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.Node; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.healthcheck.TopicHealthCheck; +import org.onap.policy.common.parameters.topic.TopicParameters; + +class KafkaHealthCheckTest { + + @Test + void testAdminClient() { + var param = new TopicParameters(); + param.setServers(List.of("localhost")); + var healthCheck = new KafkaHealthCheck(param); + var result = healthCheck.healthCheck(List.of()); + assertFalse(result); + + param.setAdditionalProps(Map.of("key", "value")); + result = healthCheck.healthCheck(List.of()); + assertFalse(result); + } + + @Test + void testMockAdminClientWithError() throws ExecutionException, InterruptedException { + var param = new TopicParameters(); + param.setServers(List.of("localhost")); + var adminClient = mock(AdminClient.class); + KafkaFuture> kafkaFuture = mock(KafkaFuture.class); + var describeCluster = mock(DescribeClusterResult.class); + when(describeCluster.nodes()).thenReturn(kafkaFuture); + when(adminClient.describeCluster()).thenReturn(describeCluster); + when(kafkaFuture.get()).thenThrow(new InterruptedException()); + var healthCheck = createKafkaHealthCheck(adminClient, param); + var result = healthCheck.healthCheck(List.of()); + Assertions.assertFalse(result); + } + + @Test + void testMockAdminClient() { + var param = new TopicParameters(); + var adminClient = mock(AdminClient.class); + // no server address + var healthCheck = createKafkaHealthCheck(adminClient, param); + var result = healthCheck.healthCheck(List.of()); + Assertions.assertTrue(result); + + param.setServers(List.of()); + result = healthCheck.healthCheck(List.of()); + Assertions.assertTrue(result); + + // no node Kafka + param.setServers(List.of("localhost")); + healthCheck = createKafkaHealthCheck(adminClient, param); + var describeCluster = mock(DescribeClusterResult.class); + when(describeCluster.nodes()).thenReturn(KafkaFuture.completedFuture(null)); + when(adminClient.describeCluster()).thenReturn(describeCluster); + result = healthCheck.healthCheck(List.of()); + Assertions.assertFalse(result); + + // Kafka is UP + var node = new Node(1, "localhost", 9092); + when(describeCluster.nodes()).thenReturn(KafkaFuture.completedFuture(List.of(node))); + result = healthCheck.healthCheck(List.of()); + Assertions.assertTrue(result); + + // Kafka topics not available + var listTopics = mock(ListTopicsResult.class); + when(adminClient.listTopics()).thenReturn(listTopics); + when(listTopics.names()).thenReturn(KafkaFuture.completedFuture(Set.of())); + result = healthCheck.healthCheck(List.of("topic")); + Assertions.assertFalse(result); + + when(listTopics.names()).thenReturn(KafkaFuture.completedFuture(Set.of("topic"))); + result = healthCheck.healthCheck(List.of("wrongTopic")); + Assertions.assertFalse(result); + + // Kafka topics available + result = healthCheck.healthCheck(List.of("topic")); + Assertions.assertTrue(result); + } + + private TopicHealthCheck createKafkaHealthCheck(AdminClient adminClient, TopicParameters param) { + return new KafkaHealthCheck(param) { + @Override + protected AdminClient createAdminClient() { + return adminClient; + } + }; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheckTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheckTest.java new file mode 100644 index 000000000..fdd3e1b93 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheckTest.java @@ -0,0 +1,85 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.healthcheck.noop; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.CommonTestData; +import org.onap.policy.common.message.bus.event.TopicEndpoint; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; + +class NoopHealthCheckTest { + + @Test + void testBuild() { + TopicEndpoint topicEndpoint = TopicEndpointManager.getManager(); + topicEndpoint.start(); + var healthCheck = new NoopHealthCheck(); + var result = healthCheck.healthCheck(List.of()); + assertTrue(result); + } + + @Test + void testBuild_Failure() { + TopicEndpoint topicEndpoint = TopicEndpointManager.getManager(); + topicEndpoint.start(); + var healthCheck = new NoopHealthCheck(); + topicEndpoint.stop(); + var result = healthCheck.healthCheck(List.of()); + assertFalse(result); + } + + @Test + void test_TopicIsAlive() { + TopicEndpoint topicEndpoint = TopicEndpointManager.getManager(); + + var topicSource = CommonTestData.getTopicParameters("topicSource", "noop", "localhost"); + var topicSink = CommonTestData.getTopicParameters("topicSink", "noop", "localhost"); + + topicEndpoint.addTopicSources(List.of(topicSource)); + topicEndpoint.addTopicSinks(List.of(topicSink)); + + topicEndpoint.start(); + var healthCheck = new NoopHealthCheck(); + var result = healthCheck.healthCheck(List.of("topicSource", "topicSink")); + assertTrue(result); + } + + @Test + void test_TopicIsNotAlive() { + TopicEndpoint topicEndpoint = TopicEndpointManager.getManager(); + + var topicSource = CommonTestData.getTopicParameters("topicSource", "noop", "localhost"); + var topicSink = CommonTestData.getTopicParameters("topicSink", "noop", "localhost"); + + topicEndpoint.addTopicSources(List.of(topicSource)); + topicEndpoint.addTopicSinks(List.of(topicSink)); + + topicEndpoint.start(); + + var topic = topicEndpoint.getNoopTopicSource("topicsource"); + topic.stop(); + var healthCheck = new NoopHealthCheck(); + var result = healthCheck.healthCheck(List.of("topicSource", "topicSink")); + assertFalse(result); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java new file mode 100644 index 000000000..55f6a69f3 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ADDITIONAL_PROPS_SUFFIX; + +import java.util.Properties; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.properties.PropertyUtils; + +class KafkaPropertyUtilsTest { + + @Test + void test() { + var properties = new Properties(); + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, "{444-"); + PropertyUtils props = new PropertyUtils(properties, "mytopic", null); + + var build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().isEmpty()); + + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, + "{\"security.protocol\": \"SASL_PLAINTEXT\"}"); + build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().containsKey("security.protocol")); + + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, + "{\"security.protocol\": false }"); + build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().isEmpty()); + + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, ""); + build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().isEmpty()); + } + +} \ No newline at end of file diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java new file mode 100644 index 000000000..df621f5b8 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java @@ -0,0 +1,269 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.features.NetLoggerFeatureApi; +import org.onap.policy.common.message.bus.features.NetLoggerFeatureProviders; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.slf4j.Logger; + +/** + * Test class for network log utilities such as logging and feature invocation. + */ +class NetLoggerUtilTest { + + private static final String TEST_TOPIC = "test-topic"; + private static final String MESSAGE = "hello world!"; + /** + * Test feature used for junits. + */ + private static NetLoggerFeature netLoggerFeature; + + /** + * Obtains the test implementation of NetLoggerFeatureApi. + */ + @BeforeAll + public static void setUp() { + netLoggerFeature = (NetLoggerFeature) NetLoggerFeatureProviders.getProviders().getList().get(0); + } + + /** + * Clears events list and resets return/exceptions flags before invoking every unit test. + */ + @BeforeEach + public void reset() { + TestAppender.clear(); + netLoggerFeature.setReturnValue(false, false); + netLoggerFeature.setExceptions(false, false); + } + + /** + * Tests obtaining the network logger instance. + */ + @Test + void getNetworkLoggerTest() { + assertEquals("network", NetLoggerUtil.getNetworkLogger().getName()); + } + + /** + * Tests logging a message to the network logger and invoking features before/after logging. + */ + @Test + void logTest() { + NetLoggerUtil.log(EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(3, TestAppender.events.size()); + } + + /** + * Tests that the network logger is used to log messages if a logger is not passed in. + */ + @Test + void logDefaultTest() { + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(3, TestAppender.events.size()); + assertEquals("network", TestAppender.events.get(0).getLoggerName()); + } + + /** + * Tests a NetLoggerFeature that replaces base implementation before logging. + */ + @Test + void beforeLogReturnTrueTest() { + netLoggerFeature.setReturnValue(true, false); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(1, TestAppender.events.size()); + } + + /** + * Tests a NetLoggerFeature that post processes a logged message. + */ + @Test + void afterLogReturnTrueTest() { + netLoggerFeature.setReturnValue(false, true); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(3, TestAppender.events.size()); + } + + /** + * Tests throwing an exception in the before hook. + */ + @Test + void beforeLogExceptionTest() { + netLoggerFeature.setExceptions(true, false); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(2, TestAppender.events.size()); + } + + /** + * Tests throwing an exception in the after hook. + */ + @Test + void afterLogExceptionTest() { + netLoggerFeature.setExceptions(false, true); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(2, TestAppender.events.size()); + } + + /** + * A custom list appender to track messages being logged to the network logger. + * NOTE: Check src/test/resources/logback-test.xml for network logger configurations. + */ + public static class TestAppender extends AppenderBase { + + /** + * List of logged events. + */ + private static final List events = new ArrayList<>(); + + /** + * Called after every unit test to clear list of events. + */ + public static void clear() { + events.clear(); + } + + /** + * Appends each event to the event list. + */ + @Override + protected void append(ILoggingEvent event) { + events.add(event); + } + + } + + /** + * Test implementation of NetLoggerFeatureApi to be used by junits. + */ + public static class NetLoggerFeature implements NetLoggerFeatureApi { + + /** + * Used for setting the return values of before/after hooks. + */ + private boolean beforeReturn = false; + private boolean afterReturn = false; + + /** + * Used for throwing an exception in the before/after hooks. + */ + private boolean beforeException = false; + private boolean afterException = false; + + + /** + * Gets sequence number. + */ + @Override + public int getSequenceNumber() { + return 0; + } + + /** + * Get beforeLog return value. + */ + public boolean getBeforeReturn() { + return this.beforeReturn; + } + + /** + * Get afterLog return value. + */ + public boolean getAfterReturn() { + return this.afterReturn; + } + + /** + * Sets the return value for the before/after hooks. + * + * @param beforeVal beforeLog() return value + * @param afterVal afterLog() return value + */ + public void setReturnValue(boolean beforeVal, boolean afterVal) { + this.beforeReturn = beforeVal; + this.afterReturn = afterVal; + } + + /** + * Gets beforeException boolean. + */ + public boolean getBeforeException() { + return this.beforeException; + } + + /** + * Gets afterException boolean. + */ + public boolean getAfterException() { + return this.afterException; + } + + /** + * Sets before/after flags to determine if the feature should throw an exception. + */ + public void setExceptions(boolean beforeException, boolean afterException) { + this.beforeException = beforeException; + this.afterException = afterException; + } + + /** + * Simple beforeLog message. + */ + @Override + public boolean beforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + + if (beforeException) { + throw new RuntimeException("beforeLog exception"); + } + + eventLogger.info("before feature test"); + + return this.beforeReturn; + } + + /** + * Simple afterLog message. + */ + @Override + public boolean afterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + + if (afterException) { + throw new RuntimeException("afterLog exception"); + } + + eventLogger.info("after feature test"); + + return this.afterReturn; + } + + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/ParameterGroupTest.java b/policy-common/src/test/java/org/onap/policy/common/parameters/ParameterGroupTest.java new file mode 100644 index 000000000..d90b13d4a --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/ParameterGroupTest.java @@ -0,0 +1,93 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ParameterGroupTest { + + private ParameterGroup parameterGroup; + + @BeforeEach + void setUp() { + parameterGroup = new ParameterGroup() { + private String name; + private BeanValidationResult validationResult = new BeanValidationResult("testGroup", "testObject"); + + @Override + public String getName() { + return name; + } + + @Override + public void setName(final String name) { + this.name = name; + } + + @Override + public BeanValidationResult validate() { + return validationResult; + } + }; + } + + @Test + void testGetName() { + String testName = "TestGroupName"; + parameterGroup.setName(testName); + assertEquals(testName, parameterGroup.getName(), "The group name should match the one set"); + } + + @Test + void testSetName() { + String testName = "AnotherGroupName"; + parameterGroup.setName(testName); + assertEquals(testName, parameterGroup.getName(), "The group name should match the one set"); + } + + @Test + void testValidate() { + BeanValidationResult result = parameterGroup.validate(); + assertNotNull(result, "The validation result should not be null"); + assertEquals("testGroup", result.getName(), "The validation result should have the correct group name"); + } + + @Test + void testIsValid() { + BeanValidationResult mockValidationResult = mock(BeanValidationResult.class); + ValidationStatus mockStatus = mock(ValidationStatus.class); + + when(mockStatus.isValid()).thenReturn(true); + when(mockValidationResult.getStatus()).thenReturn(mockStatus); + + ParameterGroup mockedParameterGroup = spy(parameterGroup); + doReturn(mockValidationResult).when(mockedParameterGroup).validate(); + + assertTrue(mockedParameterGroup.isValid(), "The parameters should be valid"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java new file mode 100644 index 000000000..ab733aed7 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java @@ -0,0 +1,214 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestBeanValidationResult { + private static final String TEXT1 = "abc"; + private static final String TEXT2 = "def"; + private static final String MY_LIST = "my-list"; + private static final String MY_MAP = "my-map"; + private static final String OBJECT = "an object"; + private static final String INITIAL_INDENT = "xx "; + private static final String NEXT_INDENT = "yy "; + private static final String MID_INDENT = "xx yy "; + private static final String NAME = "my-name"; + private static final String MY_LIST_INVALID = " 'my-list' INVALID, item has status INVALID\n "; + private static final String MY_MAP_INVALID = " 'my-map' INVALID, item has status INVALID\n "; + private static final String BEAN_INVALID_MSG = requote("'my-name' INVALID, item has status INVALID\n"); + + private String cleanMsg; + private String invalidMsg; + + private BeanValidationResult bean; + private ObjectValidationResult clean; + private ObjectValidationResult invalid; + + /** + * Sets up. + */ + @BeforeEach + void setUp() { + clean = new ObjectValidationResult(TEXT1, 10); + cleanMsg = clean.getResult("", "", true); + + invalid = new ObjectValidationResult(TEXT2, 20); + invalid.setResult(ValidationStatus.INVALID, "invalid"); + invalidMsg = invalid.getResult(); + + bean = new BeanValidationResult(NAME, OBJECT); + } + + @Test + void testBeanValidationResult() { + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + } + + @Test + void testAddResult_testGetResult() { + // null should be ok + assertTrue(bean.addResult(null)); + + assertTrue(bean.addResult(clean)); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + assertFalse(bean.addResult(invalid)); + assertFalse(bean.isValid()); + assertEquals(BEAN_INVALID_MSG + " " + invalidMsg, bean.getResult()); + + assertEquals(INITIAL_INDENT + BEAN_INVALID_MSG + MID_INDENT + cleanMsg + MID_INDENT + invalidMsg, + bean.getResult(INITIAL_INDENT, NEXT_INDENT, true)); + + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.addResult(MY_LIST, "hello", ValidationStatus.INVALID, TEXT1)); + assertThat(bean.getResult()).contains("\"" + MY_LIST + "\" value \"hello\" INVALID, " + TEXT1); + } + + @Test + void testValidateNotNull() { + assertTrue(bean.validateNotNull("sub-name", "sub-object")); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + assertFalse(bean.validateNotNull("sub-name", null)); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + " item 'sub-name' value 'null' INVALID, is null\n"), bean.getResult()); + } + + @Test + void testValidateNotNullList() { + List list = List.of(clean); + assertTrue(bean.validateNotNullList(MY_LIST, list, item -> item)); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + list = Arrays.asList(invalid, invalid); + assertFalse(bean.validateNotNullList(MY_LIST, list, item -> item)); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_LIST_INVALID + invalidMsg + + " " + invalidMsg), bean.getResult()); + } + + @Test + void testValidateNotNullList_NullList() { + List list = null; + assertFalse(bean.validateNotNullList(MY_LIST, list, item -> item)); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + " item 'my-list' value 'null' INVALID, is null\n"), bean.getResult()); + + } + + @Test + void testValidateList() { + List list = null; + bean = new BeanValidationResult(NAME, OBJECT); + assertTrue(bean.validateList(MY_LIST, list, item -> item)); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + list = List.of(clean); + bean = new BeanValidationResult(NAME, OBJECT); + assertTrue(bean.validateList(MY_LIST, list, item -> item)); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + // null item in the list + list = Arrays.asList(clean, null); + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.validateList(MY_LIST, list, item -> item)); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_LIST_INVALID + + "item 'item' value 'null' INVALID, null\n"), bean.getResult()); + + list = Arrays.asList(invalid, invalid); + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.validateList(MY_LIST, list, item -> item)); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_LIST_INVALID + invalidMsg + + " " + invalidMsg), bean.getResult()); + + } + + @Test + void testValidateMap() { + Map map = null; + bean = new BeanValidationResult(NAME, OBJECT); + assertTrue(bean.validateMap(MY_MAP, map, validMapEntry())); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + map = Map.of(TEXT1, clean, TEXT2, clean); + bean = new BeanValidationResult(NAME, OBJECT); + assertTrue(bean.validateMap(MY_MAP, map, validMapEntry())); + assertTrue(bean.isValid()); + assertNull(bean.getResult()); + + // null value in the map + map = new TreeMap<>(); + map.put(TEXT1, clean); + map.put(TEXT2, null); + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.validateMap(MY_MAP, map, validMapEntry())); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_MAP_INVALID + + "item 'def' value 'null' INVALID, is null\n"), bean.getResult()); + + map = Map.of(TEXT1, invalid, TEXT2, invalid); + bean = new BeanValidationResult(NAME, OBJECT); + assertFalse(bean.validateMap(MY_MAP, map, validMapEntry())); + assertFalse(bean.isValid()); + assertEquals(requote(BEAN_INVALID_MSG + MY_MAP_INVALID + invalidMsg + + " " + invalidMsg), bean.getResult()); + + } + + private BiConsumer> validMapEntry() { + return (result, entry) -> { + var value = entry.getValue(); + if (value == null) { + result.validateNotNull(entry.getKey(), value); + } else { + result.addResult(value); + } + }; + } + + private static String requote(String text) { + return text.replace('\'', '"'); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java new file mode 100644 index 000000000..c4211136e --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java @@ -0,0 +1,661 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import lombok.Getter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.annotations.ClassName; +import org.onap.policy.common.parameters.annotations.Max; +import org.onap.policy.common.parameters.annotations.Min; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; +import org.onap.policy.common.parameters.annotations.Pattern; +import org.onap.policy.common.parameters.annotations.Size; +import org.onap.policy.common.parameters.annotations.Valid; + +class TestBeanValidator { + private static final String TOP = "top"; + private static final String STR_FIELD = "strValue"; + private static final String INT_FIELD = "intValue"; + private static final String NUM_FIELD = "numValue"; + private static final String ITEMS_FIELD = "items"; + private static final String STRING_VALUE = "string value"; + private static final int INT_VALUE = 20; + + private BeanValidator validator; + + @BeforeEach + void setUp() { + validator = new BeanValidator(); + } + + @Test + void testValidateTop_testValidateFields() { + // validate null + assertTrue(validator.validateTop(TOP, null).isValid()); + + // validate something that has no annotations + assertTrue(validator.validateTop(TOP, validator).isValid()); + + @NotNull + @Getter + class Data { + String strValue; + } + + // one failure case + Data data = new Data(); + BeanValidationResult result = validator.validateTop(TOP, data); + assertInvalid("testValidateFields", result, STR_FIELD, "null"); + assertTrue(result.getResult().contains(TOP)); + + // one success case + data.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, data).isValid()); + + @Getter + class Derived extends Data { + @Min(10) + int intValue; + } + + Derived derived = new Derived(); + derived.strValue = STRING_VALUE; + derived.intValue = INT_VALUE; + + // success case + assertTrue(validator.validateTop(TOP, derived).isValid()); + + // failure cases + derived.strValue = null; + assertInvalid("testValidateFields", validator.validateTop(TOP, derived), STR_FIELD, "null"); + derived.strValue = STRING_VALUE; + + derived.intValue = 1; + assertInvalid("testValidateFields", validator.validateTop(TOP, derived), INT_FIELD, "minimum"); + derived.intValue = INT_VALUE; + + // both invalid + derived.strValue = null; + derived.intValue = 1; + result = validator.validateTop(TOP, derived); + assertInvalid("testValidateFields", result, STR_FIELD, "null"); + assertInvalid("testValidateFields", result, INT_FIELD, "minimum"); + derived.strValue = STRING_VALUE; + derived.intValue = INT_VALUE; + } + + @Test + void testVerNotNull() { + @Getter + class NotNullCheck { + @Min(1) + @NotNull + Integer intValue; + } + + NotNullCheck notNullCheck = new NotNullCheck(); + assertInvalid("testVerNotNull", validator.validateTop(TOP, notNullCheck), INT_FIELD, "null"); + + notNullCheck.intValue = INT_VALUE; + assertTrue(validator.validateTop(TOP, notNullCheck).isValid()); + + notNullCheck.intValue = 0; + assertInvalid("testVerNotNull", validator.validateTop(TOP, notNullCheck), INT_FIELD, "minimum"); + } + + @Test + void testVerNotBlank() { + @Getter + class NotBlankCheck { + @NotBlank + String strValue; + } + + NotBlankCheck notBlankCheck = new NotBlankCheck(); + + // null + assertTrue(validator.validateTop(TOP, notBlankCheck).isValid()); + + // empty + notBlankCheck.strValue = ""; + assertInvalid("testVerNotNull", validator.validateTop(TOP, notBlankCheck), STR_FIELD, "blank"); + + // spaces + notBlankCheck.strValue = " "; + assertInvalid("testVerNotNull", validator.validateTop(TOP, notBlankCheck), STR_FIELD, "blank"); + + // not blank + notBlankCheck.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, notBlankCheck).isValid()); + + /* + * Class with "blank" annotation on an integer. + */ + @Getter + class NotBlankInt { + @NotBlank + int intValue; + } + + NotBlankInt notBlankInt = new NotBlankInt(); + notBlankInt.intValue = 0; + assertTrue(validator.validateTop(TOP, notBlankInt).isValid()); + } + + /** + * Tests verSize with a collection. + */ + @Test + void testVerSizeCollection() { + @Getter + class CollectionSizeCheck { + @Size(min = 3) + Collection items; + } + + CollectionSizeCheck collCheck = new CollectionSizeCheck(); + + // valid length - exact + collCheck.items = List.of(1, 2, 3); + assertThat(validator.validateTop(TOP, collCheck).isValid()).isTrue(); + + // valid length - extra + collCheck.items = List.of(1, 2, 3, 4); + assertThat(validator.validateTop(TOP, collCheck).isValid()).isTrue(); + + // too few + collCheck.items = List.of(1, 2); + assertInvalid("testVerSize", validator.validateTop(TOP, collCheck), ITEMS_FIELD, "minimum", "3"); + + // null + collCheck.items = null; + assertThat(validator.validateTop(TOP, collCheck).isValid()).isTrue(); + } + + /** + * Tests verSize with a map. + */ + @Test + void testVerSizeMap() { + @Getter + class MapSizeCheck { + @Size(min = 3) + Map items; + } + + MapSizeCheck mapCheck = new MapSizeCheck(); + + // valid length - exact + mapCheck.items = Map.of(1, 10, 2, 20, 3, 30); + assertThat(validator.validateTop(TOP, mapCheck).isValid()).isTrue(); + + // valid length - extra + mapCheck.items = Map.of(1, 10, 2, 20, 3, 30, 4, 40); + assertThat(validator.validateTop(TOP, mapCheck).isValid()).isTrue(); + + // too few + mapCheck.items = Map.of(1, 10, 2, 20); + assertInvalid("testVerSize", validator.validateTop(TOP, mapCheck), ITEMS_FIELD, "minimum", "3"); + + // null + mapCheck.items = null; + assertThat(validator.validateTop(TOP, mapCheck).isValid()).isTrue(); + } + + /** + * Tests verSize with an object for which it doesn't apply. + */ + @Test + void testVerSizeOther() { + @Getter + class OtherSizeCheck { + @Size(min = 3) + Integer items; + } + + OtherSizeCheck otherCheck = new OtherSizeCheck(); + + otherCheck.items = 10; + assertThat(validator.validateTop(TOP, otherCheck).isValid()).isTrue(); + } + + @Test + void testVerRegex() { + @Getter + class RegexCheck { + @Pattern(regexp = "[a-f]*") + String strValue; + } + + RegexCheck regexCheck = new RegexCheck(); + + // does not match + regexCheck.strValue = "xyz"; + assertInvalid("testVerRegex", validator.validateTop(TOP, regexCheck), STR_FIELD, + "does not match regular expression [a-f]"); + + // matches + regexCheck.strValue = "abcabc"; + assertTrue(validator.validateTop(TOP, regexCheck).isValid()); + + // invalid regex + @Getter + class InvalidRegexCheck { + @Pattern(regexp = "[a-f") + String strValue; + } + + InvalidRegexCheck invalidRegexCheck = new InvalidRegexCheck(); + + // does not match + invalidRegexCheck.strValue = "abc"; + assertInvalid("testVerRegex", validator.validateTop(TOP, invalidRegexCheck), STR_FIELD, + "does not match regular expression [a-f"); + + // matches + regexCheck.strValue = "abcabc"; + assertTrue(validator.validateTop(TOP, regexCheck).isValid()); + + /* + * Class with "regex" annotation on an integer. + */ + @Getter + class RegexInt { + @Pattern(regexp = "[a-f]*") + int intValue; + } + + RegexInt regexInt = new RegexInt(); + regexInt.intValue = 0; + assertInvalid("testVerRegex", validator.validateTop(TOP, regexInt), INT_FIELD, + "does not match regular expression [a-f]"); + } + + @Test + void testVerMax() { + /* + * Field is not a number. + */ + @Getter + class NonNumeric { + @Max(100) + String strValue; + } + + NonNumeric nonNumeric = new NonNumeric(); + nonNumeric.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, nonNumeric).isValid()); + + /* + * Integer field. + */ + @Getter + class IntField { + @Max(100) + Integer intValue; + } + + // ok value + IntField intField = new IntField(); + assertNumeric(intField, value -> { + intField.intValue = value; + }, INT_FIELD, "maximum", 100, 101); + + /* + * Long field. + */ + @Getter + class LongField { + @Max(100) + Long numValue; + } + + // ok value + LongField longField = new LongField(); + assertNumeric(longField, value -> { + longField.numValue = (long) value; + }, NUM_FIELD, "maximum", 100, 101); + + /* + * Float field. + */ + @Getter + class FloatField { + @Max(100) + Float numValue; + } + + // ok value + FloatField floatField = new FloatField(); + assertNumeric(floatField, value -> { + floatField.numValue = (float) value; + }, NUM_FIELD, "maximum", 100, 101); + + /* + * Double field. + */ + @Getter + class DoubleField { + @Max(100) + Double numValue; + } + + // ok value + DoubleField doubleField = new DoubleField(); + assertNumeric(doubleField, value -> { + doubleField.numValue = (double) value; + }, NUM_FIELD, "maximum", 100, 101); + + /* + * Atomic Integer field (which is a subclass of Number). + */ + @Getter + class AtomIntValue { + @Max(100) + AtomicInteger numValue; + } + + // ok value + AtomIntValue atomIntField = new AtomIntValue(); + atomIntField.numValue = new AtomicInteger(INT_VALUE); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + + // invalid value - should be OK, because it isn't an Integer + atomIntField.numValue.set(101); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + } + + @Test + void testVerMin() { + /* + * Field is not a number. + */ + @Getter + class NonNumeric { + @Min(10) + String strValue; + } + + NonNumeric nonNumeric = new NonNumeric(); + nonNumeric.strValue = STRING_VALUE; + assertTrue(validator.validateTop(TOP, nonNumeric).isValid()); + + /* + * Integer field. + */ + @Getter + class IntField { + @Min(10) + Integer intValue; + } + + // ok value + IntField intField = new IntField(); + assertNumeric(intField, value -> { + intField.intValue = value; + }, INT_FIELD, "minimum", 10, 1); + + /* + * Long field. + */ + @Getter + class LongField { + @Min(10) + Long numValue; + } + + // ok value + LongField longField = new LongField(); + assertNumeric(longField, value -> { + longField.numValue = (long) value; + }, NUM_FIELD, "minimum", 10, 1); + + /* + * Float field. + */ + @Getter + class FloatField { + @Min(10) + Float numValue; + } + + // ok value + FloatField floatField = new FloatField(); + assertNumeric(floatField, value -> { + floatField.numValue = (float) value; + }, NUM_FIELD, "minimum", 10, 1); + + /* + * Double field. + */ + @Getter + class DoubleField { + @Min(10) + Double numValue; + } + + // ok value + DoubleField doubleField = new DoubleField(); + assertNumeric(doubleField, value -> { + doubleField.numValue = (double) value; + }, NUM_FIELD, "minimum", 10, 1); + + /* + * Atomic Integer field (which is a subclass of Number). + */ + @Getter + class AtomIntValue { + @Min(10) + AtomicInteger numValue; + } + + // ok value + AtomIntValue atomIntField = new AtomIntValue(); + atomIntField.numValue = new AtomicInteger(INT_VALUE); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + + // invalid value - should be OK, because it isn't an Integer + atomIntField.numValue.set(101); + assertTrue(validator.validateTop(TOP, atomIntField).isValid()); + } + + @Test + void testVerClassName() { + @Getter + class ClassNameCheck { + @ClassName + String strValue; + } + + ClassNameCheck classCheck = new ClassNameCheck(); + + // null should be OK + classCheck.strValue = null; + assertTrue(validator.validateTop(TOP, classCheck).isValid()); + + // valid class name + classCheck.strValue = getClass().getName(); + assertTrue(validator.validateTop(TOP, classCheck).isValid()); + + // invalid class name + classCheck.strValue = ""; + assertInvalid("testVerClassName", validator.validateTop(TOP, classCheck), + STR_FIELD, "class is not in the classpath"); + } + + @Test + void testVerCascade() { + @Getter + class Item { + @NotNull + Integer intValue; + } + + @Getter + class Container { + @Valid + Item checked; + + Item unchecked; + + @Valid + List items; + + @Valid + Map itemMap; + } + + Container cont = new Container(); + cont.unchecked = new Item(); + cont.items = List.of(new Item()); + cont.itemMap = Map.of(STRING_VALUE, new Item()); + + cont.checked = null; + assertTrue(validator.validateTop(TOP, cont).isValid()); + + cont.checked = new Item(); + + assertInvalid("testVerCascade", validator.validateTop(TOP, cont), INT_FIELD, "null"); + + cont.checked.intValue = INT_VALUE; + assertTrue(validator.validateTop(TOP, cont).isValid()); + } + + @Test + void testVerCollection() { + @Getter + class Container { + List<@Min(5) Integer> items; + + // not a collection - should not be checked + @Valid + String strValue; + + String noAnnotations; + } + + Container cont = new Container(); + cont.strValue = STRING_VALUE; + cont.noAnnotations = STRING_VALUE; + + // null collection - always valid + assertTrue(validator.validateTop(TOP, cont).isValid()); + + // empty collection - always valid + cont.items = List.of(); + assertTrue(validator.validateTop(TOP, cont).isValid()); + + cont.items = List.of(-10, -20); + assertThat(validator.validateTop(TOP, cont).getResult()).contains("\"0\"", "-10", "\"1\"", "-20", "minimum"); + + cont.items = List.of(10, -30); + assertThat(validator.validateTop(TOP, cont).getResult()).contains("\"1\"", "-30", "minimum") + .doesNotContain("\"0\""); + + cont.items = List.of(10, 20); + assertTrue(validator.validateTop(TOP, cont).isValid()); + } + + @Test + void testVerMap() { + @Getter + class Container { + Map items; + + // not a map + @NotBlank + String strValue; + + String noAnnotations; + } + + Container cont = new Container(); + cont.strValue = STRING_VALUE; + cont.noAnnotations = STRING_VALUE; + + // null map - always valid + assertTrue(validator.validateTop(TOP, cont).isValid()); + + // empty map - always valid + cont.items = Map.of(); + assertTrue(validator.validateTop(TOP, cont).isValid()); + + cont.items = Map.of("abc", -10, "def", -20); + assertThat(validator.validateTop(TOP, cont).getResult()).contains("abc", "-10", "def", "-20", "minimum"); + + cont.items = Map.of("abc", 10, "def", -30); + assertThat(validator.validateTop(TOP, cont).getResult()).contains("def", "-30", "minimum") + .doesNotContain("abc"); + + cont.items = Map.of("abc", 10, "def", 20); + assertTrue(validator.validateTop(TOP, cont).isValid()); + } + + @Test + void testGetEntryName() { + assertThat(validator.getEntryName(makeEntry(null))).isEmpty(); + assertThat(validator.getEntryName(makeEntry(""))).isEmpty(); + assertThat(validator.getEntryName(makeEntry(STRING_VALUE))).isEqualTo(STRING_VALUE); + } + + /** + * Makes a Map entry with the given key and value. + * + * @param key desired key + * @return a new Map entry + */ + private Map.Entry makeEntry(String key) { + HashMap map = new HashMap<>(); + map.put(key, 0); + return map.entrySet().iterator().next(); + } + + private void assertNumeric(T object, Consumer setter, String fieldName, + String expectedText, int edge, int outside) { + setter.accept(TestBeanValidator.INT_VALUE); + assertTrue(validator.validateTop(TOP, object).isValid()); + + // on the edge + setter.accept(edge); + assertTrue(validator.validateTop(TOP, object).isValid()); + + // invalid + setter.accept(outside); + assertInvalid("testVerNotNull", validator.validateTop(TOP, object), fieldName, expectedText); + } + + + private void assertInvalid(String testName, BeanValidationResult result, String... text) { + assertThat(result.getResult()).describedAs(testName).contains(text); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestFieldValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestFieldValidator.java new file mode 100644 index 000000000..0659ad81b --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestFieldValidator.java @@ -0,0 +1,379 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.gson.annotations.SerializedName; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.annotations.Min; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; + +class TestFieldValidator extends ValidatorUtil { + private static final String INT_LIST_FIELD = "intList"; + private static final String INT_MAP_FIELD = "intMap"; + private static final String UNANNOTATED_FIELD = "unannotated"; + private static final String INT_FIELD = "intValue"; + private static final int VALID_INT = 10; + private static final int INVALID_INT = -10; + + @Getter + private int unannotated; + + @Min(0) + @Getter + private int intValue; + + @Getter + private List<@Min(1) Integer> intList; + + @Getter + private Map<@NotBlank String, @Min(1) Integer> intMap; + + @SerializedName("annotated_key_map") + @Getter + private Map<@NotBlank String, Integer> annotatedKeyMap; + + @Getter + private Map annotatedValueMap; + + @Getter + private List unannotatedList; + + @Getter + private Map unannotatedMap; + + @NotNull + @Getter + private boolean boolValue; + + @NotNull + @Getter + private String notNullValue; + + @Min(0) + @Getter + private static int staticField; + + /** + * Has no accessor. + */ + @Min(0) + private int noMethod; + + /** + * Accessor is {@link #getStaticMethod()}, which is static. + */ + @Min(0) + private int staticMethod; + + /** + * Accessor is {@link #getVoidMethod()}, which returns a void. + */ + @Min(0) + private int voidMethod; + + /** + * Accessor is {@link #getParameterizedMethod(boolean)}, which requires a parameter. + */ + @Min(0) + private int parameterizedMethod; + + /** + * Accessor is {@link #getExMethod()}, which throws an exception. + */ + @Min(0) + private int exMethod; + + + @BeforeEach + void setUp() { + bean = new BeanValidator(); + } + + @Test + void testGetAnnotation() { + // field-level annotation + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse(); + + // class-level annotation + assertThat(new FieldValidator(bean, ClassAnnot.class, getField(ClassAnnot.class, "text")).isEmpty()).isFalse(); + } + + @Test + void testFieldValidator() throws NoSuchFieldException, SecurityException { + /* + * Note: nested classes contain fields like "$this", thus the check for "$" in the + * variable name is already covered by the other tests. + */ + + /* + * Class with no annotations. + */ + @NotNull + class NoAnnotations { + @SuppressWarnings("unused") + String strValue; + } + + Field field = NoAnnotations.class.getDeclaredField("this$0"); + + assertThat(new FieldValidator(bean, NoAnnotations.class, field).isEmpty()).isTrue(); + + // unannotated + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("unannotated")).isEmpty()).isTrue(); + + // these are invalid for various reasons + + Field staticField2 = getField("staticField"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticField2)) + .isInstanceOf(IllegalArgumentException.class); + + Field noMethodField = getField("noMethod"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, noMethodField)) + .isInstanceOf(IllegalArgumentException.class); + + // annotated + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse(); + } + + @Test + void testFieldValidator_SetNullAllowed() { + // default - null is allowed + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isNullAllowed()).isTrue(); + + // field-level NotNull + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("notNullValue")).isNullAllowed()) + .isFalse(); + + // class-level NotNull + assertThat(new FieldValidator(bean, ClassAnnot.class, getField(ClassAnnot.class, "noMethod")).isNullAllowed()) + .isFalse(); + } + + @Test + void testAddListValidator() { + + // unannotated + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("unannotatedList")).isEmpty()).isTrue(); + + // annotated + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_LIST_FIELD)).isEmpty()).isFalse(); + } + + @Test + void testAddMapValidator() { + + // unannotated + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("unannotatedMap")).isEmpty()).isTrue(); + + // annotated + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_MAP_FIELD)).isEmpty()).isFalse(); + + // only the key is annotated + FieldValidator validator = new FieldValidator(bean, TestFieldValidator.class, getField("annotatedKeyMap")); + assertThat(validator.isEmpty()).isFalse(); + + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + annotatedKeyMap = Map.of("abc", -10); + validator.validateField(result, this); + assertThat(result.getResult()).isNull(); + + annotatedKeyMap = Map.of(" ", -10); + validator.validateField(result, this); + assertThat(result.getResult()).contains("annotated_key_map", "blank").doesNotContain("-10"); + + // only the value is annotated + validator = new FieldValidator(bean, TestFieldValidator.class, getField("annotatedValueMap")); + assertThat(validator.isEmpty()).isFalse(); + + result = new BeanValidationResult(MY_NAME, this); + annotatedValueMap = Map.of(" ", 10); + validator.validateField(result, this); + assertThat(result.getResult()).isNull(); + + annotatedValueMap = Map.of(" ", -10); + validator.validateField(result, this); + assertThat(result.getResult()).doesNotContain("blank").contains("annotatedValueMap", "\" \"", "-10"); + } + + @SuppressWarnings("deprecation") + @Test + void testValidateField_testGetValue() { + // unannotated + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + new FieldValidator(bean, getClass(), getField(UNANNOTATED_FIELD)).validateField(result, this); + assertThat(result.getResult()).isNull(); + + // valid + intValue = VALID_INT; + result = new BeanValidationResult(MY_NAME, this); + new FieldValidator(bean, getClass(), getField(INT_FIELD)).validateField(result, this); + assertThat(result.getResult()).isNull(); + + // invalid + intValue = INVALID_INT; + result = new BeanValidationResult(MY_NAME, this); + new FieldValidator(bean, getClass(), getField(INT_FIELD)).validateField(result, this); + assertThat(result.getResult()).contains(INT_FIELD); + + // throws an exception + FieldValidator validator = new FieldValidator(bean, TestFieldValidator.class, getField("exMethod")); + BeanValidationResult result2 = new BeanValidationResult(MY_NAME, this); + assertThatThrownBy(() -> validator.validateField(result2, this)).isInstanceOf(IllegalArgumentException.class) + .getCause().isInstanceOf(InvocationTargetException.class).getCause() + .hasMessage("expected exception"); + } + + @Test + void testValidateField_testGetValue_ListField() { + // valid + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + intList = List.of(10, 20, 30, 40); + new FieldValidator(bean, getClass(), getField(INT_LIST_FIELD)).validateField(result, this); + assertThat(result.getResult()).isNull(); + + // invalid + result = new BeanValidationResult(MY_NAME, this); + intList = List.of(9, -8, 7, -6); + new FieldValidator(bean, getClass(), getField(INT_LIST_FIELD)).validateField(result, this); + assertThat(result.getResult()).doesNotContain("0", "9").contains("1", "-8").doesNotContain("2", "7") + .contains("3", "-6"); + } + + @Test + void testValidateField_testGetValue_MapField() { + // valid + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + intMap = Map.of("ten", 10, "twenty", 20, "thirty", 30, "forty", 40); + new FieldValidator(bean, getClass(), getField(INT_MAP_FIELD)).validateField(result, this); + assertThat(result.getResult()).isNull(); + + // invalid + result = new BeanValidationResult(MY_NAME, this); + intMap = Map.of("ten", 9, "twenty", -8, "thirty", 7, "forty", -6); + new FieldValidator(bean, getClass(), getField(INT_MAP_FIELD)).validateField(result, this); + assertThat(result.getResult()).doesNotContain("ten", "9").contains("twenty", "-8").doesNotContain("thirty", "7") + .contains("forty", "-6"); + } + + @Test + void testClassOnly() { + // class-level annotation has no bearing on a static field + assertThat(new FieldValidator(bean, ClassAnnot.class, getField(ClassAnnot.class, "staticValue")).isEmpty()) + .isTrue(); + + // field-level annotation on a static field + Field staticField2 = getField("staticField"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticField2)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testGetAccessor() { + // uses "getXxx" + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse(); + + // uses "isXxx" + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("boolValue")).isEmpty()).isFalse(); + } + + @Test + void testGetMethod() { + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse(); + + // these are invalid for various reasons + + Field noMethodField = getField("noMethod"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, noMethodField)) + .isInstanceOf(IllegalArgumentException.class); + + Field staticMethodField = getField("staticMethod"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticMethodField)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testValidMethod() { + assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse(); + + // these are invalid for various reasons + + Field staticMethodField = getField("staticMethod"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticMethodField)) + .isInstanceOf(IllegalArgumentException.class); + + Field voidMethodField = getField("voidMethod"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, voidMethodField)) + .isInstanceOf(IllegalArgumentException.class); + + Field paramMethodField = getField("parameterizedMethod"); + assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, paramMethodField)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testIsFieldAnnotated_testSetFieldAnnotated() { + // annotated at the field level + assertThat(new FieldValidator(bean, getClass(), getField(INT_FIELD)).isFieldAnnotated()).isTrue(); + + // unannotated + assertThat(new FieldValidator(bean, getClass(), getField(UNANNOTATED_FIELD)).isFieldAnnotated()).isFalse(); + } + + public static int getStaticMethod() { + return -1000; + } + + void getVoidMethod() { + // do nothing + } + + public int getParameterizedMethod(boolean flag) { + return flag ? 0 : 1; + } + + public int getExMethod() { + throw new RuntimeException("expected exception"); + } + + @NotNull + public static class ClassAnnot { + @Getter + private String text; + + // no "get" method + @SuppressWarnings("unused") + private String noMethod; + + @Getter + private static int staticValue; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestItemValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestItemValidator.java new file mode 100644 index 000000000..b2686fdfd --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestItemValidator.java @@ -0,0 +1,122 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.annotations.Min; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; + +class TestItemValidator extends ValidatorUtil { + + // annotated fields - each field must have exactly one annotation + + /** + * This annotation does not contain a method returning an array. + */ + @Min(value = 0) + private int notArray; + + /** + * This annotation doesn't contain any annotations that the {@link BeanValidator} + * recognizes. + */ + @Simple + private int mismatch; + + /** + * No annotations. + */ + @SuppressWarnings("unused") + private int noAnnotations; + + /** + * One matching annotation. + */ + @NotNull + private int match; + + /** + * Multiple matching annotations. + */ + @NotNull + @NotBlank + private String multiMatch; + + + @BeforeEach + void setUp() { + bean = new BeanValidator(); + } + + @Test + void testGetAnnotation() { + // no matches + assertThat(new ItemValidator(bean, getAnnotType("noAnnotations"), true).isEmpty()).isTrue(); + + // had a match + assertThat(new ItemValidator(bean, getAnnotType("match"), true).checkers).hasSize(1); + + // multiple matches + ItemValidator validator = new ItemValidator(bean, getAnnotType("multiMatch"), true); + assertThat(validator.checkers).hasSize(2); + + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + validator.validateValue(result, MY_FIELD, HELLO); + assertThat(result.getResult()).isNull(); + + result = new BeanValidationResult(MY_NAME, this); + validator.validateValue(result, MY_FIELD, null); + assertThat(result.getResult()).isNotNull(); + + result = new BeanValidationResult(MY_NAME, this); + validator.validateValue(result, MY_FIELD, ""); + assertThat(result.getResult()).isNotNull(); + } + + @Test + void testItemValidatorBeanValidatorAnnotation() { + assertThat(new ItemValidator(bean, getAnnotType("match")).isEmpty()).isFalse(); + } + + @Test + void testItemValidatorBeanValidatorAnnotationBoolean() { + assertThat(new ItemValidator(bean, getAnnotType("match"), true).isEmpty()).isFalse(); + + assertThat(new ItemValidator(bean, getAnnotType("match"), false).isEmpty()).isTrue(); + } + + // these annotations are not recognized by the BeanValidator + + @Retention(RUNTIME) + @Target(FIELD) + public @interface Simple { + + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestObjectValidationResult.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestObjectValidationResult.java new file mode 100644 index 000000000..c72d4f4a0 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestObjectValidationResult.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class TestObjectValidationResult { + private static final String NAME = "my-name"; + private static final Object OBJECT = "my-object"; + + private ObjectValidationResult result; + + @Test + void testValidationResultImplStringObjectValidationStatusString() { + result = new ObjectValidationResult(NAME, OBJECT, ValidationStatus.INVALID, "invalid data"); + assertEquals(NAME, result.getName()); + assertEquals(OBJECT, result.getObject()); + assertEquals(ValidationStatus.INVALID, result.getStatus()); + assertEquals("invalid data", result.getMessage()); + } + + @Test + void testGetResult() { + result = new ObjectValidationResult(NAME, OBJECT); + assertEquals(ValidationStatus.CLEAN, result.getStatus()); + assertNull(result.getResult()); + assertEquals(requote("xxx item 'my-name' value 'my-object' CLEAN, item has status CLEAN\n"), + result.getResult("xxx ", "yyy", true)); + + result.setResult(ValidationStatus.WARNING, "a warning"); + assertEquals(ValidationStatus.WARNING, result.getStatus()); + + // should not override warning + result.setResult(ValidationStatus.OBSERVATION, "an observation"); + assertEquals(ValidationStatus.WARNING, result.getStatus()); + + assertTrue(result.isValid()); + assertEquals(requote("item 'my-name' value 'my-object' WARNING, a warning\n"), result.getResult()); + + result.setResult(ValidationStatus.INVALID, "is invalid"); + assertEquals(ValidationStatus.INVALID, result.getStatus()); + + assertFalse(result.isValid()); + assertEquals(requote("item 'my-name' value 'my-object' INVALID, is invalid\n"), result.getResult()); + } + + private String requote(String text) { + return text.replace('\'', '"'); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestValidationResultImpl.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestValidationResultImpl.java new file mode 100644 index 000000000..f6851b5ef --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestValidationResultImpl.java @@ -0,0 +1,123 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestValidationResultImpl { + private static final String NAME = "my-name"; + private static final Object OBJECT = "my-object"; + + private MyResult result; + + @BeforeEach + void setUp() { + result = new MyResult(NAME, OBJECT); + } + + @Test + void testValidationResultImplStringObjectValidationStatusString() { + result = new MyResult(NAME, OBJECT, ValidationStatus.INVALID, "invalid data"); + assertEquals(NAME, result.getName()); + assertEquals(OBJECT, result.getObject()); + assertEquals(ValidationStatus.INVALID, result.getStatus()); + assertEquals("invalid data", result.getMessage()); + } + + @Test + void testValidateNotNull() { + assertTrue(result.validateNotNull()); + assertTrue(result.isValid()); + assertNull(result.getResult()); + + // now try with null + result = new MyResult(NAME, null); + assertFalse(result.validateNotNull()); + assertFalse(result.isValid()); + assertEquals("INVALID is null", result.getResult()); + } + + @Test + void testSetResultValidationStatus() { + result.setResult(ValidationStatus.WARNING); + assertEquals(ValidationStatus.WARNING, result.getStatus()); + + // should not override warning + result.setResult(ValidationStatus.OBSERVATION); + assertEquals(ValidationStatus.WARNING, result.getStatus()); + + assertTrue(result.isValid()); + assertEquals("WARNING item has status WARNING", result.getResult()); + } + + @Test + void testSetResult_testGetResult_testGetStatus() { + assertEquals(ValidationStatus.CLEAN, result.getStatus()); + assertEquals("CLEAN item has status CLEAN", result.getResult("xxx ", "yyy", true)); + + result.setResult(ValidationStatus.WARNING, "a warning"); + assertEquals(ValidationStatus.WARNING, result.getStatus()); + + // should not override warning + result.setResult(ValidationStatus.OBSERVATION, "an observation"); + assertEquals(ValidationStatus.WARNING, result.getStatus()); + + assertTrue(result.isValid()); + assertEquals("WARNING a warning", result.getResult()); + + result.setResult(ValidationStatus.INVALID, "is invalid"); + assertEquals(ValidationStatus.INVALID, result.getStatus()); + + assertFalse(result.isValid()); + assertEquals("INVALID is invalid", result.getResult()); + } + + @Test + void testGetName() { + assertEquals(NAME, result.getName()); + } + + private static class MyResult extends ValidationResultImpl { + public MyResult(String name, Object object) { + super(name, object); + } + + public MyResult(String name, Object object, ValidationStatus status, String message) { + super(name, object, status, message); + } + + @Override + public String getResult(String initialIndentation, String subIndentation, boolean showClean) { + if (!showClean && getStatus() == ValidationStatus.CLEAN) { + return null; + } + + return (getStatus() + " " + getMessage()); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestValueValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestValueValidator.java new file mode 100644 index 000000000..1b42876ab --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/TestValueValidator.java @@ -0,0 +1,140 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024-2025 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Annotation; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.annotations.Min; +import org.onap.policy.common.parameters.annotations.NotBlank; +import org.onap.policy.common.parameters.annotations.NotNull; + +class TestValueValidator extends ValidatorUtil { + + private ValueValidator validator; + + // these fields just provide place-holders for annotations + + @NotNull + @NotBlank + private final int annotField = 1; + + + @BeforeEach + void setUp() { + validator = new MyValueValidator(); + } + + @Test + void testIsEmpty() { + assertThat(validator.isEmpty()).isTrue(); + + validator.addAnnotation(NotNull.class, (result2, fieldName, value) -> true); + assertThat(validator.isEmpty()).isFalse(); + } + + @Test + void testValidateValue_NullValue() { + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + + validator.validateValue(result, MY_FIELD, null); + assertThat(result.getResult()).isNull(); + + validator.addAnnotation(NotNull.class, BeanValidationResult::validateNotNull); + validator.validateValue(result, MY_FIELD, null); + assertThat(result.getResult()).contains(MY_FIELD, "null"); + } + + @Test + void testValidateValue_NotNullValue() { + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + + validator.validateValue(result, MY_FIELD, HELLO); + assertThat(result.getResult()).isNull(); + + validator.addAnnotation(NotNull.class, BeanValidationResult::validateNotNull); + validator.validateValue(result, MY_FIELD, HELLO); + assertThat(result.getResult()).isNull(); + } + + @Test + void testAddAnnotationClassOfTChecker() { + // the field does not have this annotation + validator.addAnnotation(Min.class, (result2, fieldName, value) -> true); + assertThat(validator.isEmpty()).isTrue(); + + // "null" flag should stay true with this annotation + assertThat(validator.isNullAllowed()).isTrue(); + validator.addAnnotation(NotBlank.class, (result2, fieldName, value) -> true); + assertThat(validator.isNullAllowed()).isTrue(); + + // "null" flag should become false with this annotation + validator.addAnnotation(NotNull.class, (result2, fieldName, value) -> true); + assertThat(validator.isNullAllowed()).isFalse(); + } + + @Test + void testAddAnnotationClassOfTCheckerWithAnnotOfT() { + // the field does not have this annotation + validator.addAnnotation(Min.class, (result2, fieldName, annot, value) -> true); + assertThat(validator.isEmpty()).isTrue(); + + // indicates the annotation value + AtomicBoolean wasNull = new AtomicBoolean(false); + + // the field DOES have this annotation + validator.addAnnotation(NotNull.class, (result2, fieldName, annot, value) -> { + wasNull.set(annot != null); + return result2.validateNotNull(fieldName, value); + }); + assertThat(validator.isEmpty()).isFalse(); + + // ensure that the checker is invoked + BeanValidationResult result = new BeanValidationResult(MY_NAME, this); + validator.validateValue(result, MY_FIELD, HELLO); + assertThat(result.getResult()).isNull(); + + assertThat(wasNull.get()).isTrue(); + } + + @Test + void testGetAnnotation() { + assertThat(new ValueValidator().getAnnotation(NotNull.class)).isNull(); + } + + /** + * Checks for annotations on the "annotField" field. + */ + private static class MyValueValidator extends ValueValidator { + @Override + public T getAnnotation(Class annotClass) { + try { + return TestValueValidator.class.getDeclaredField("annotField").getAnnotation(annotClass); + } catch (NoSuchFieldException | SecurityException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/ValidatorUtil.java b/policy-common/src/test/java/org/onap/policy/common/parameters/ValidatorUtil.java new file mode 100644 index 000000000..e39b5b86c --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/ValidatorUtil.java @@ -0,0 +1,83 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Field; + +/** + * Utilities for validator tests. + */ +public class ValidatorUtil { + protected static final String MY_NAME = "My-Name"; + protected static final String MY_FIELD = "My-Field"; + protected static final String HELLO = "hello"; + + protected BeanValidator bean; + + /** + * Gets the single annotation for a given field. + * + * @param fieldName name of the field having the desired annotation + * @return the given field's annotation + */ + protected Annotation getAnnot(String fieldName) { + return getField(fieldName).getAnnotations()[0]; + } + + /** + * Gets the annotated type for a given field. + * + * @param fieldName name of the field of interest + * @return the given field's annotated type + */ + protected AnnotatedType getAnnotType(String fieldName) { + return getField(fieldName).getAnnotatedType(); + } + + /** + * Gets a field from this object. + * + * @param fieldName name of the field of interest + * @return the given field + */ + protected Field getField(String fieldName) { + return getField(getClass(), fieldName); + } + + /** + * Gets a field from a given class. + * + * @param clazz class containing the field + * @param fieldName name of the field of interest + * @return the given field + */ + protected Field getField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + + } catch (NoSuchFieldException | SecurityException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/topic/BusTopicParamsTest.java b/policy-common/src/test/java/org/onap/policy/common/parameters/topic/BusTopicParamsTest.java new file mode 100644 index 000000000..c474e5fcc --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/topic/BusTopicParamsTest.java @@ -0,0 +1,201 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.topic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.topic.BusTopicParams.TopicParamsBuilder; + +class BusTopicParamsTest { + + public static final String MY_AFT_ENV = "my-aft-env"; + public static final String MY_API_KEY = "my-api-key"; + public static final String MY_API_SECRET = "my-api-secret"; + public static final String MY_BASE_PATH = "my-base"; + public static final String MY_CLIENT_NAME = "my-client"; + public static final String MY_CONS_GROUP = "my-cons-group"; + public static final String MY_CONS_INST = "my-cons-inst"; + public static final String MY_ENV = "my-env"; + public static final int MY_FETCH_LIMIT = 100; + public static final int MY_FETCH_TIMEOUT = 101; + public static final String MY_HOST = "my-host"; + public static final String MY_LAT = "my-lat"; + public static final String MY_LONG = "my-long"; + public static final String MY_PARTNER = "my-partner"; + public static final String MY_PASS = "my-pass"; + public static final int MY_PORT = 102; + public static final String MY_TOPIC = "my-topic"; + public static final String MY_EFFECTIVE_TOPIC = "my-effective-topic"; + public static final String MY_USERNAME = "my-user"; + public static final String MY_PARTITION = "my-partition"; + public static final String MY_SERIALIZER = "org.apache.kafka.common.serialization.StringSerializer"; + + protected Map addProps; + protected TopicParamsBuilder builder; + + @BeforeEach + public void setUp() { + addProps = new TreeMap<>(); + addProps.put("my-key-A", "my-value-A"); + addProps.put("my-key-B", "my-value-B"); + + builder = makeBuilder(); + } + + @Test + void testGetters() { + BusTopicParams params = makeBuilder().build(); + + Assertions.assertEquals(addProps, params.getAdditionalProps()); + Assertions.assertEquals(MY_AFT_ENV, params.getAftEnvironment()); + assertTrue(params.isAllowSelfSignedCerts()); + Assertions.assertEquals(MY_API_KEY, params.getApiKey()); + Assertions.assertEquals(MY_API_SECRET, params.getApiSecret()); + Assertions.assertEquals(MY_BASE_PATH, params.getBasePath()); + Assertions.assertEquals(MY_CLIENT_NAME, params.getClientName()); + Assertions.assertEquals(MY_CONS_GROUP, params.getConsumerGroup()); + Assertions.assertEquals(MY_CONS_INST, params.getConsumerInstance()); + Assertions.assertEquals(MY_ENV, params.getEnvironment()); + Assertions.assertEquals(MY_FETCH_LIMIT, params.getFetchLimit()); + Assertions.assertEquals(MY_FETCH_TIMEOUT, params.getFetchTimeout()); + Assertions.assertEquals(MY_HOST, params.getHostname()); + Assertions.assertEquals(MY_LAT, params.getLatitude()); + Assertions.assertEquals(MY_LONG, params.getLongitude()); + assertTrue(params.isManaged()); + Assertions.assertEquals(MY_PARTITION, params.getPartitionId()); + Assertions.assertEquals(MY_PARTNER, params.getPartner()); + Assertions.assertEquals(MY_PASS, params.getPassword()); + Assertions.assertEquals(MY_PORT, params.getPort()); + Assertions.assertEquals(List.of("localhost"), params.getServers()); + Assertions.assertEquals(MY_TOPIC, params.getTopic()); + Assertions.assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + assertTrue(params.isUseHttps()); + Assertions.assertEquals(MY_USERNAME, params.getUserName()); + } + + @Test + void testBooleanGetters() { + // ensure that booleans are independent of each other + testBoolean("true:false:false", TopicParamsBuilder::allowSelfSignedCerts); + testBoolean("false:true:false", TopicParamsBuilder::managed); + testBoolean("false:false:true", TopicParamsBuilder::useHttps); + } + + @Test + void testValidators() { + BusTopicParams params = makeBuilder().build(); + + // test validity methods + assertTrue(params.isAdditionalPropsValid()); + assertFalse(params.isAftEnvironmentInvalid()); + assertTrue(params.isApiKeyValid()); + assertTrue(params.isApiSecretValid()); + assertFalse(params.isClientNameInvalid()); + assertFalse(params.isConsumerGroupInvalid()); + assertFalse(params.isConsumerInstanceInvalid()); + assertFalse(params.isEnvironmentInvalid()); + assertFalse(params.isHostnameInvalid()); + assertFalse(params.isLatitudeInvalid()); + assertFalse(params.isLongitudeInvalid()); + assertFalse(params.isPartitionIdInvalid()); + assertFalse(params.isPartnerInvalid()); + assertTrue(params.isPasswordValid()); + assertFalse(params.isPortInvalid()); + assertFalse(params.isServersInvalid()); + assertFalse(params.isTopicInvalid()); + assertTrue(params.isUserNameValid()); + } + + @Test + void testInvertedValidators() { + Assertions.assertFalse(makeBuilder().additionalProps(null).build().isAdditionalPropsValid()); + Assertions.assertTrue(makeBuilder().aftEnvironment("").build().isAftEnvironmentInvalid()); + Assertions.assertFalse(makeBuilder().apiKey("").build().isApiKeyValid()); + Assertions.assertFalse(makeBuilder().apiSecret("").build().isApiSecretValid()); + Assertions.assertTrue(makeBuilder().clientName("").build().isClientNameInvalid()); + Assertions.assertTrue(makeBuilder().consumerGroup("").build().isConsumerGroupInvalid()); + Assertions.assertTrue(makeBuilder().consumerInstance("").build().isConsumerInstanceInvalid()); + Assertions.assertTrue(makeBuilder().environment("").build().isEnvironmentInvalid()); + Assertions.assertTrue(makeBuilder().hostname("").build().isHostnameInvalid()); + Assertions.assertTrue(makeBuilder().latitude("").build().isLatitudeInvalid()); + Assertions.assertTrue(makeBuilder().longitude("").build().isLongitudeInvalid()); + Assertions.assertTrue(makeBuilder().partitionId("").build().isPartitionIdInvalid()); + Assertions.assertTrue(makeBuilder().partner("").build().isPartnerInvalid()); + Assertions.assertFalse(makeBuilder().password("").build().isPasswordValid()); + Assertions.assertTrue(makeBuilder().port(-1).build().isPortInvalid()); + Assertions.assertTrue(makeBuilder().port(65536).build().isPortInvalid()); + Assertions.assertTrue(makeBuilder().servers(null).build().isServersInvalid()); + Assertions.assertTrue(makeBuilder().servers(new LinkedList<>()).build().isServersInvalid()); + Assertions.assertTrue(makeBuilder().servers(List.of("")).build().isServersInvalid()); + Assertions.assertFalse(makeBuilder().servers(List.of("one-server")).build().isServersInvalid()); + Assertions.assertTrue(makeBuilder().topic("").build().isTopicInvalid()); + Assertions.assertFalse(makeBuilder().userName("").build().isUserNameValid()); + } + + /** + * Tests the boolean methods by applying a function, once with {@code false} and once + * with {@code true}. Verifies that all the boolean methods return the correct + * value by concatenating them. + * + * @param expectedTrue the string that is expected when {@code true} is passed to the + * method + * @param function function to be applied to the builder + */ + private void testBoolean(String expectedTrue, BiConsumer function) { + TopicParamsBuilder topicParamsBuilder = BusTopicParams.builder(); + + // first try the "false" case + function.accept(topicParamsBuilder, false); + + BusTopicParams params = topicParamsBuilder.build(); + assertEquals("false:false:false", + params.isAllowSelfSignedCerts() + ":" + params.isManaged() + ":" + params.isUseHttps()); + + + // now try the "true" case + function.accept(topicParamsBuilder, true); + + params = topicParamsBuilder.build(); + assertEquals(expectedTrue, + params.isAllowSelfSignedCerts() + ":" + params.isManaged() + ":" + params.isUseHttps()); + } + + public TopicParamsBuilder makeBuilder() { + + return BusTopicParams.builder().additionalProps(addProps).aftEnvironment(MY_AFT_ENV).allowSelfSignedCerts(true) + .apiKey(MY_API_KEY).apiSecret(MY_API_SECRET).basePath(MY_BASE_PATH).clientName(MY_CLIENT_NAME) + .consumerGroup(MY_CONS_GROUP).consumerInstance(MY_CONS_INST).environment(MY_ENV) + .fetchLimit(MY_FETCH_LIMIT).fetchTimeout(MY_FETCH_TIMEOUT).hostname(MY_HOST).latitude(MY_LAT) + .longitude(MY_LONG).managed(true).partitionId(MY_PARTITION).partner(MY_PARTNER) + .password(MY_PASS).port(MY_PORT).servers(List.of("localhost")).topic(MY_TOPIC) + .effectiveTopic(MY_EFFECTIVE_TOPIC).useHttps(true).allowTracing(true).userName(MY_USERNAME) + .serializationProvider(MY_SERIALIZER); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/validation/ParameterGroupValidatorTest.java b/policy-common/src/test/java/org/onap/policy/common/parameters/validation/ParameterGroupValidatorTest.java new file mode 100644 index 000000000..0c7f29b7b --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/parameters/validation/ParameterGroupValidatorTest.java @@ -0,0 +1,91 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.parameters.validation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.validation.ConstraintValidatorContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.parameters.BeanValidationResult; +import org.onap.policy.common.parameters.ParameterGroup; + +class ParameterGroupValidatorTest { + + private ParameterGroupValidator validator; + + @Mock + private ParameterGroup mockParameterGroup; + + @Mock + private BeanValidationResult mockBeanValidationResult; + + @Mock + private ConstraintValidatorContext mockContext; + + @Mock + private ConstraintValidatorContext.ConstraintViolationBuilder mockViolationBuilder; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + validator = new ParameterGroupValidator(); + } + + @Test + void testIsValid_NullValue() { + boolean result = validator.isValid(null, mockContext); + assertTrue(result, "Expected isValid to return true when value is null"); + } + + @Test + void testIsValid_ValidParameterGroup() { + when(mockParameterGroup.validate()).thenReturn(mockBeanValidationResult); + when(mockBeanValidationResult.isValid()).thenReturn(true); + + boolean result = validator.isValid(mockParameterGroup, mockContext); + assertTrue(result, "Expected isValid to return true when ParameterGroup is valid"); + + verify(mockContext, never()).buildConstraintViolationWithTemplate(anyString()); + } + + @Test + void testIsValid_InvalidParameterGroup() { + when(mockParameterGroup.validate()).thenReturn(mockBeanValidationResult); + when(mockBeanValidationResult.isValid()).thenReturn(false); + when(mockBeanValidationResult.getMessage()).thenReturn("Invalid parameters"); + when(mockContext.buildConstraintViolationWithTemplate(anyString())).thenReturn(mockViolationBuilder); + + boolean result = validator.isValid(mockParameterGroup, mockContext); + assertFalse(result, "Expected isValid to return false when ParameterGroup is invalid"); + + InOrder inOrder = inOrder(mockContext, mockViolationBuilder); + inOrder.verify(mockContext).buildConstraintViolationWithTemplate("Invalid parameters"); + inOrder.verify(mockViolationBuilder).addConstraintViolation(); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategyTest.java b/policy-common/src/test/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategyTest.java new file mode 100644 index 000000000..e7c3ad5d7 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategyTest.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.spring.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ImplicitJoinColumnNameSource; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +class CustomImplicitNamingStrategyTest { + + static CustomImplicitNamingStrategy strategy; + + @Mock + static ImplicitJoinColumnNameSource source; + + @BeforeAll + public static void setUpBeforeClass() { + strategy = new CustomImplicitNamingStrategy(); + source = mock(ImplicitJoinColumnNameSource.class); + } + + @Test + void testDetermineJoinColumnName() { + Identifier identifier = new Identifier("identifier", true); + + MetadataBuildingContext buildingContextMock = mock(MetadataBuildingContext.class); + InFlightMetadataCollector flightCollectorMock = mock(InFlightMetadataCollector.class); + Database databaseMock = mock(Database.class); + + when(flightCollectorMock.getDatabase()).thenReturn(databaseMock); + when(source.getReferencedColumnName()).thenReturn(identifier); + when(source.getBuildingContext()).thenReturn(buildingContextMock); + when(buildingContextMock.getMetadataCollector()).thenReturn(flightCollectorMock); + + JdbcEnvironment environmentMock = mock(JdbcEnvironment.class); + when(databaseMock.getJdbcEnvironment()).thenReturn(environmentMock); + + IdentifierHelper helperMock = mock(IdentifierHelper.class); + when(environmentMock.getIdentifierHelper()).thenReturn(helperMock); + when(helperMock.toIdentifier(anyString())).thenReturn(identifier); + + Identifier result = strategy.determineJoinColumnName(source); + assertEquals(identifier, result); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverterTest.java b/policy-common/src/test/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverterTest.java new file mode 100644 index 000000000..0c4674125 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverterTest.java @@ -0,0 +1,136 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.spring.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +class YamlHttpMessageConverterTest { + + private YamlHttpMessageConverter converter; + + @BeforeEach + void setUp() { + converter = new YamlHttpMessageConverter(); + } + + @Test + void testCanReadAndWriteYamlMediaType() { + MediaType mediaType = new MediaType("application", "yaml"); + assertTrue(converter.canRead(Object.class, mediaType)); + assertTrue(converter.canWrite(Object.class, mediaType)); + } + + @Test + void testReadInternal() throws IOException { + // YAML content representing a simple key-value pair as a map + String yamlContent = "key: value"; + + // Mocking HttpHeaders + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getContentType()).thenReturn(MediaType.APPLICATION_JSON); // Return JSON media type + + // Mocking HttpInputMessage + HttpInputMessage inputMessage = mock(HttpInputMessage.class); + when(inputMessage.getBody()).thenReturn(new ByteArrayInputStream(yamlContent.getBytes(StandardCharsets.UTF_8))); + when(inputMessage.getHeaders()).thenReturn(headers); + + // Now we call the converter's read method and assert the results + Map result = (Map) converter.read(Map.class, null, inputMessage); + + assertNotNull(result); + assertEquals("value", result.get("key")); + } + + + @Test + void testReadInternalWithException() throws IOException { + HttpInputMessage inputMessage = mock(HttpInputMessage.class); + when(inputMessage.getBody()).thenThrow(new IOException("IO Exception during reading")); + + assertThrows(HttpMessageNotReadableException.class, () -> converter.read(Map.class, null, inputMessage)); + } + + @Test + void testWriteInternal() throws IOException { + // Mocking HttpHeaders + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getContentType()).thenReturn(MediaType.APPLICATION_JSON); // Return JSON media type + when(headers.getAcceptCharset()).thenReturn(null); // Return null to use default charset + + // Mocking HttpOutputMessage + HttpOutputMessage outputMessage = mock(HttpOutputMessage.class); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + when(outputMessage.getBody()).thenReturn(outputStream); + when(outputMessage.getHeaders()).thenReturn(headers); + + // A simple map to be serialized into YAML + Map map = new HashMap<>(); + map.put("key", "value"); + + // Calling the converter's write method + converter.write(map, null, outputMessage); + + // Verifying the output + String result = outputStream.toString(StandardCharsets.UTF_8); + assertTrue(result.contains("key: value")); + } + + + @Test + void testWriteInternalWithException() throws IOException { + // Mocking HttpHeaders + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getContentType()).thenReturn(MediaType.APPLICATION_JSON); // Return YAML media type + + // Mocking HttpOutputMessage to throw an IOException when getBody() is called + HttpOutputMessage outputMessage = mock(HttpOutputMessage.class); + when(outputMessage.getBody()).thenThrow(new IOException("IO Exception during writing")); + when(outputMessage.getHeaders()).thenReturn(headers); + + // A simple map to be serialized into YAML + Map map = new HashMap<>(); + map.put("key", "value"); + + // We expect the write method to throw a HttpMessageNotWritableException + assertThrows(HttpMessageNotWritableException.class, () -> converter.write(map, null, outputMessage)); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderExceptionTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderExceptionTest.java new file mode 100644 index 000000000..9e82c5d87 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderExceptionTest.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class CoderExceptionTest { + private static final String STRING_VALUE = "My String"; + private static final Throwable CAUSE = new Throwable(); + + private CoderException exc; + + @Test + void testCoderException() { + exc = new CoderException(); + + assertNull(exc.getMessage()); + assertSame(null, exc.getCause()); + assertNotNull(exc.toString()); + } + + @Test + void testCoderExceptionString() { + exc = new CoderException(STRING_VALUE); + + assertEquals(STRING_VALUE, exc.getMessage()); + assertSame(null, exc.getCause()); + assertTrue(exc.toString().contains(STRING_VALUE)); + } + + @Test + void testCoderExceptionThrowable() { + exc = new CoderException(CAUSE); + + assertEquals(CAUSE.toString(), exc.getMessage()); + assertSame(CAUSE, exc.getCause()); + assertNotNull(exc.toString()); + } + + @Test + void testCoderExceptionStringThrowable() { + exc = new CoderException(STRING_VALUE, CAUSE); + + assertEquals(STRING_VALUE, exc.getMessage()); + assertSame(CAUSE, exc.getCause()); + assertTrue(exc.toString().contains(STRING_VALUE)); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderTest.java new file mode 100644 index 000000000..fe72292a1 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderTest.java @@ -0,0 +1,130 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CoderTest { + private static final Long LONG = 10L; + private static final Integer INTEGER = 10; + private static final String INT_TEXT = INTEGER.toString(); + private static final String TEXT = "some text"; + private static final String ENCODED = "encoded value"; + private static final String DECODED = "decoded value"; + + private MyCoder coder; + + @BeforeEach + void setUp() { + coder = new MyCoder(); + } + + @Test + void testConvert() throws CoderException { + assertNull(coder.convert(null, String.class)); + + // same class of object + assertEquals(TEXT, coder.convert(TEXT, String.class)); + assertEquals(INTEGER, coder.convert(INTEGER, Integer.class)); + + // source is a string + assertEquals(INTEGER, coder.convert(TEXT, Integer.class)); + + // target is a string + assertEquals(INT_TEXT, coder.convert(INTEGER, String.class)); + + // source and target are different types, neither is a string + assertEquals(INTEGER, coder.convert(LONG, Integer.class)); + } + + private static class MyCoder implements Coder { + @Override + public String encode(Object object) throws CoderException { + return (object.getClass() == String.class ? ENCODED : INT_TEXT); + } + + @Override + public String encode(Object object, boolean pretty) throws CoderException { + // unused + return null; + } + + @Override + public void encode(Writer target, Object object) throws CoderException { + // unused + } + + @Override + public void encode(OutputStream target, Object object) throws CoderException { + // unused + } + + @Override + public void encode(File target, Object object) throws CoderException { + // unused + } + + @Override + public T decode(String json, Class clazz) throws CoderException { + return (clazz == String.class ? clazz.cast(DECODED) : clazz.cast(INTEGER)); + } + + @Override + public T decode(Reader source, Class clazz) throws CoderException { + // unused + return null; + } + + @Override + public T decode(InputStream source, Class clazz) throws CoderException { + // unused + return null; + } + + @Override + public T decode(File source, Class clazz) throws CoderException { + // unused + return null; + } + + @Override + public StandardCoderObject toStandard(Object object) throws CoderException { + // unused + return null; + } + + @Override + public T fromStandard(StandardCoderObject sco, Class clazz) throws CoderException { + // unused + return null; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderObjectTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderObjectTest.java new file mode 100644 index 000000000..0e4967be2 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderObjectTest.java @@ -0,0 +1,131 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024-2025 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StandardCoderObjectTest { + private static final Gson gson = new Gson(); + + private static final String PROP1 = "abc"; + private static final String PROP2 = "ghi"; + private static final Integer PROP2_INDEX = 1; + private static final String PROP_2_B = "jkl"; + private static final String VAL1 = "def"; + private static final String VAL2 = "mno"; + private static final String JSON = "{'abc':'def','ghi':[{},{'jkl':'mno'}]}".replace('\'', '"'); + + private StandardCoderObject sco; + + /** + * Creates a standard object, populated with some data. + * + * @throws Exception if an error occurs + */ + @BeforeEach + public void setUp() throws Exception { + sco = new StandardCoderObject(gson.fromJson(JSON, JsonElement.class)); + } + + @Test + void testStandardCoderObject() { + assertNull(new StandardCoderObject().getData()); + } + + @Test + void testStandardCoderObjectJsonElement() { + assertNotNull(sco.getData()); + assertEquals(JSON, gson.toJson(sco.getData())); + } + + @Test + void testGetString() throws Exception { + // one field + assertEquals(VAL1, sco.getString(PROP1)); + + // multiple fields + assertEquals(VAL2, sco.getString(PROP2, PROP2_INDEX, PROP_2_B)); + + // not found + assertNull(sco.getString("xyz")); + + // read from null object + assertNull(new StandardCoderObject().getString()); + assertNull(new StandardCoderObject().getString(PROP1)); + + JsonElement obj = gson.fromJson("{'abc':[]}".replace('\'', '"'), JsonElement.class); + sco = new StandardCoderObject(obj); + + // not a primitive + assertNull(sco.getString(PROP1)); + + // not a JSON object + assertNull(sco.getString(PROP1, PROP2)); + + // invalid subscript + assertThatIllegalArgumentException().isThrownBy(() -> sco.getString(10.0)); + } + + @Test + void testGetFieldFromObject() { + // not an object + assertNull(sco.getFieldFromObject(fromJson("[]"), PROP1)); + + // field doesn't exist + assertNull(sco.getFieldFromObject(fromJson("{}"), "non-existent")); + + // field exists + assertEquals(4, sco.getFieldFromObject(fromJson("{\"world\":4}"), "world").getAsInt()); + } + + @Test + void testGetItemFromArray() { + // not an array + assertNull(sco.getItemFromArray(fromJson("{}"), 0)); + + // negative index + assertThatIllegalArgumentException().isThrownBy(() -> sco.getItemFromArray(fromJson("[]"), -1)); + + // index out of bounds + assertNull(sco.getItemFromArray(fromJson("[5]"), 1)); + assertNull(sco.getItemFromArray(fromJson("[5]"), 2)); + + // index exists + assertEquals(6, sco.getItemFromArray(fromJson("[5,6,7]"), 1).getAsInt()); + + // edge case: first and last item + assertEquals(50, sco.getItemFromArray(fromJson("[50,60,70]"), 0).getAsInt()); + assertEquals(700, sco.getItemFromArray(fromJson("[500,600,700]"), 2).getAsInt()); + } + + private JsonElement fromJson(String json) { + return gson.fromJson(json, JsonElement.class); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java new file mode 100644 index 000000000..269893e72 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java @@ -0,0 +1,376 @@ +/* + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import lombok.ToString; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StandardCoderTest { + private static final String EXPECTED_EXCEPTION = "expected exception"; + + private static final JsonParseException jpe = new JsonParseException(EXPECTED_EXCEPTION); + private static final IOException ioe = new IOException(EXPECTED_EXCEPTION); + + private StandardCoder coder; + + @BeforeEach + public void setUp() { + coder = new StandardCoder(); + } + + @Test + void testConvert() throws CoderException { + // null source + assertNull(coder.convert(null, StandardCoderObject.class)); + + // same class of object + StandardCoderObject sco = new StandardCoderObject(); + assertSame(sco, coder.convert(sco, StandardCoderObject.class)); + + // source is a string + assertEquals(Integer.valueOf(10), coder.convert("10", Integer.class)); + + // target is a string + assertEquals("10", coder.convert(10, String.class)); + + // source and target are different types, neither is a string + sco = coder.convert(Map.of("hello", "world"), StandardCoderObject.class); + assertEquals("world", sco.getString("hello")); + + // throw an exeception + coder = new StandardCoder() { + @Override + protected T fromJson(JsonElement json, Class clazz) { + throw jpe; + } + }; + assertThatThrownBy(() -> coder.convert(10, Long.class)).isInstanceOf(CoderException.class).hasCause(jpe); + } + + @Test + void testEncodeObject() throws Exception { + List arr = Arrays.asList(1100, 1110); + assertEquals("[1100,1110]", coder.encode(arr)); + + // test exception case + coder = spy(new StandardCoder()); + when(coder.toJson(arr)).thenThrow(jpe); + assertThatThrownBy(() -> coder.encode(arr)).isInstanceOf(CoderException.class).hasCause(jpe); + } + + @Test + void testEncodeObjectBoolean() throws Exception { + final List arr = Arrays.asList(1100, 1110); + + /* + * As plain json. + */ + assertEquals("[1100,1110]", coder.encode(arr, false)); + + // test exception case + coder = spy(new StandardCoder()); + when(coder.toJson(arr)).thenThrow(jpe); + assertThatThrownBy(() -> coder.encode(arr, false)).isInstanceOf(CoderException.class).hasCause(jpe); + + + /* + * As pretty json. + */ + assertEquals("[\n 1100,\n 1110\n]", coder.encode(arr, true)); + + // test exception case + coder = spy(new StandardCoder()); + when(coder.toPrettyJson(arr)).thenThrow(jpe); + assertThatThrownBy(() -> coder.encode(arr, true)).isInstanceOf(CoderException.class).hasCause(jpe); + } + + @Test + void testEncodeWriterObject() throws Exception { + List arr = Arrays.asList(1200, 1210); + StringWriter wtr = new StringWriter(); + coder.encode(wtr, arr); + assertEquals("[1200,1210]", wtr.toString()); + + // test json exception + coder = spy(new StandardCoder()); + doThrow(jpe).when(coder).toJson(wtr, arr); + assertThatThrownBy(() -> coder.encode(wtr, arr)).isInstanceOf(CoderException.class).hasCause(jpe); + } + + @Test + void testEncodeOutputStreamObject() throws Exception { + List arr = Arrays.asList(1300, 1310); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + coder.encode(stream, arr); + assertEquals("[1300,1310]", stream.toString("UTF-8")); + + // test json exception + Writer wtr = new StringWriter(); + coder = spy(new StandardCoder()); + when(coder.makeWriter(stream)).thenReturn(wtr); + doThrow(jpe).when(coder).toJson(wtr, arr); + assertThatThrownBy(() -> coder.encode(stream, arr)).isInstanceOf(CoderException.class).hasCause(jpe); + + // test exception when flushed + wtr = spy(new OutputStreamWriter(stream)); + doThrow(ioe).when(wtr).flush(); + coder = spy(new StandardCoder()); + when(coder.makeWriter(stream)).thenReturn(wtr); + assertThatThrownBy(() -> coder.encode(stream, arr)).isInstanceOf(CoderException.class).hasCause(ioe); + } + + @Test + void testEncodeFileObject() throws Exception { + File file = new File(getClass().getResource(StandardCoder.class.getSimpleName() + ".json").getFile() + "X"); + file.deleteOnExit(); + List arr = Arrays.asList(1400, 1410); + coder.encode(file, arr); + assertEquals("[1400,1410]", new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)); + + // test json exception + StringWriter wtr = new StringWriter(); + coder = spy(new StandardCoder()); + when(coder.makeWriter(file)).thenReturn(wtr); + doThrow(jpe).when(coder).toJson(wtr, arr); + assertThatThrownBy(() -> coder.encode(file, arr)).isInstanceOf(CoderException.class).hasCause(jpe); + + // test exception when closed + coder = spy(new StandardCoder()); + wtr = spy(new StringWriter()); + doThrow(ioe).when(wtr).close(); + coder = spy(new StandardCoder()); + when(coder.makeWriter(file)).thenReturn(wtr); + assertThatThrownBy(() -> coder.encode(file, arr)).isInstanceOf(CoderException.class).hasCause(ioe); + } + + @Test + void testDecodeStringClass() throws Exception { + String text = "[2200,2210]"; + assertEquals(text, coder.decode(text, JsonElement.class).toString()); + + // test json exception + coder = spy(new StandardCoder()); + when(coder.fromJson(text, JsonElement.class)).thenThrow(jpe); + assertThatThrownBy(() -> coder.decode(text, JsonElement.class)).isInstanceOf(CoderException.class) + .hasCause(jpe); + } + + @Test + void testDecodeReaderClass() throws Exception { + String text = "[2300,2310]"; + assertEquals(text, coder.decode(new StringReader(text), JsonElement.class).toString()); + + // test json exception + coder = spy(new StandardCoder()); + StringReader rdr = new StringReader(text); + when(coder.fromJson(rdr, JsonElement.class)).thenThrow(jpe); + assertThatThrownBy(() -> coder.decode(rdr, JsonElement.class)).isInstanceOf(CoderException.class).hasCause(jpe); + } + + @Test + void testDecodeInputStreamClass() throws Exception { + String text = "[2400,2410]"; + assertEquals(text, + coder.decode(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), JsonElement.class) + .toString()); + + // test json exception + coder = spy(new StandardCoder()); + ByteArrayInputStream stream = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)); + StringReader rdr = new StringReader(text); + when(coder.makeReader(stream)).thenReturn(rdr); + when(coder.fromJson(rdr, JsonElement.class)).thenThrow(jpe); + assertThatThrownBy(() -> coder.decode(stream, JsonElement.class)).isInstanceOf(CoderException.class) + .hasCause(jpe); + } + + @Test + void testDecodeFileClass() throws Exception { + File file = new File(getClass().getResource(StandardCoder.class.getSimpleName() + ".json").getFile()); + String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + assertEquals(text, coder.decode(file, JsonElement.class).toString()); + + // test FileNotFoundException case + assertThatThrownBy(() -> coder.decode(new File("unknown-file"), JsonElement.class)) + .isInstanceOf(CoderException.class).hasCauseInstanceOf(FileNotFoundException.class); + + // test json exception + Reader rdr = new StringReader(text); + coder = spy(new StandardCoder()); + when(coder.makeReader(file)).thenReturn(rdr); + when(coder.fromJson(rdr, JsonElement.class)).thenThrow(jpe); + assertThatThrownBy(() -> coder.decode(file, JsonElement.class)).isInstanceOf(CoderException.class) + .hasCause(jpe); + + // test IOException case + rdr = spy(new FileReader(file)); + doThrow(ioe).when(rdr).close(); + coder = spy(new StandardCoder()); + when(coder.makeReader(file)).thenReturn(rdr); + assertThatThrownBy(() -> coder.decode(file, JsonElement.class)).isInstanceOf(CoderException.class) + .hasCause(ioe); + } + + @Test + void testToJsonTree_testFromJsonJsonElementClassT() { + MyMap map = new MyMap(); + map.props = new LinkedHashMap<>(); + map.props.put("jel keyA", "jel valueA"); + map.props.put("jel keyB", "jel valueB"); + + JsonElement json = coder.toJsonTree(map); + assertEquals("{'props':{'jel keyA':'jel valueA','jel keyB':'jel valueB'}}".replace('\'', '"'), json.toString()); + + Object result = coder.fromJson(json, MyMap.class); + + assertNotNull(result); + assertEquals("{jel keyA=jel valueA, jel keyB=jel valueB}", result.toString()); + } + + @Test + void testConvertFromDouble() throws Exception { + String text = "[listA, {keyA=100}, 200]"; + assertEquals(text, coder.decode(text, Object.class).toString()); + + text = "{keyB=200}"; + assertEquals(text, coder.decode(text, Object.class).toString()); + } + + @Test + void testToStandard() throws Exception { + MyObject obj = new MyObject(); + obj.abc = "xyz"; + StandardCoderObject sco = coder.toStandard(obj); + assertNotNull(sco.getData()); + assertEquals("{'abc':'xyz'}".replace('\'', '"'), sco.getData().toString()); + + // class instead of object -> exception + assertThatThrownBy(() -> coder.toStandard(String.class)).isInstanceOf(CoderException.class); + } + + @Test + void testFromStandard() throws Exception { + MyObject obj = new MyObject(); + obj.abc = "pdq"; + StandardCoderObject sco = coder.toStandard(obj); + + MyObject obj2 = coder.fromStandard(sco, MyObject.class); + assertEquals(obj.toString(), obj2.toString()); + + // null class -> exception + assertThatThrownBy(() -> coder.fromStandard(sco, null)).isInstanceOf(CoderException.class); + } + + @Test + void testStandardTypeAdapter() { + String json = "{'abc':'def'}".replace('\'', '"'); + StandardCoderObject sco = coder.fromJson(json, StandardCoderObject.class); + assertNotNull(sco.getData()); + assertEquals(json, sco.getData().toString()); + assertEquals(json, coder.toJson(sco)); + + // invalid json -> exception + StringReader rdr = new StringReader("["); + assertThatThrownBy(() -> coder.fromJson(rdr, StandardCoderObject.class)) + .isInstanceOf(JsonSyntaxException.class); + } + + @Test + void testMapDouble() throws Exception { + MyMap map = new MyMap(); + map.props = new HashMap<>(); + map.props.put("plainString", "def"); + map.props.put("negInt", -10); + map.props.put("doubleVal", 12.5); + map.props.put("posLong", 100000000000L); + + String json = coder.encode(map); + + map.props.clear(); + map = coder.decode(json, MyMap.class); + + assertEquals("def", map.props.get("plainString")); + assertEquals(-10, map.props.get("negInt")); + assertEquals(100000000000L, map.props.get("posLong")); + assertEquals(12.5, map.props.get("doubleVal")); + + // test when decoding into a map + @SuppressWarnings("unchecked") + Map map2 = coder.decode("{'intValue':10, 'dblVal':20.1}", TreeMap.class); + assertEquals("{dblVal=20.1, intValue=10}", map2.toString()); + } + + @Test + void testListDouble() throws Exception { + @SuppressWarnings("unchecked") + List list = coder.decode("[10, 20.1, 30]", LinkedList.class); + assertEquals("[10, 20.1, 30]", list.toString()); + } + + + @ToString + private static class MyObject { + private String abc; + } + + public static class MyMap { + private Map props; + + @Override + public String toString() { + return props.toString(); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java new file mode 100644 index 000000000..d504b82be --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java @@ -0,0 +1,114 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.coder.YamlJsonTranslatorTest.Container; + +class StandardYamlCoderTest { + private static final File YAML_FILE = + new File("src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml"); + + private StandardYamlCoder coder; + private Container cont; + + @BeforeEach + public void setUp() throws CoderException { + coder = new StandardYamlCoder(); + cont = coder.decode(YAML_FILE, Container.class); + } + + @Test + void testToPrettyJson() throws CoderException { + String expected = coder.encode(cont); + assertEquals(expected, coder.encode(cont, false)); + + String yaml = coder.encode(cont, true); + assertEquals(expected, yaml); + + Container cont2 = coder.decode(yaml, Container.class); + assertEquals(cont, cont2); + + // test exception cases + IllegalArgumentException expex = new IllegalArgumentException("expected exception"); + coder = spy(new StandardYamlCoder()); + when(coder.toJson(cont)).thenThrow(expex); + assertThatThrownBy(() -> coder.encode(cont, false)).isInstanceOf(CoderException.class).hasCause(expex); + assertThatThrownBy(() -> coder.encode(cont, true)).isInstanceOf(CoderException.class).hasCause(expex); + } + + @Test + void testToJsonObject() throws CoderException { + String yaml = coder.encode(cont); + + Container cont2 = coder.decode(yaml, Container.class); + assertEquals(cont, cont2); + } + + @Test + void testToJsonWriterObject() throws CoderException { + StringWriter wtr = new StringWriter(); + coder.encode(wtr, cont); + String yaml = wtr.toString(); + + Container cont2 = coder.decode(yaml, Container.class); + assertEquals(cont, cont2); + } + + @Test + void testFromJsonStringClassOfT() throws Exception { + String yaml = new String(Files.readAllBytes(YAML_FILE.toPath()), StandardCharsets.UTF_8); + Container cont2 = coder.decode(yaml, Container.class); + assertEquals(cont, cont2); + } + + @Test + void testFromJsonReaderClassOfT() { + YamlJsonTranslatorTest.verify(cont); + } + + @Test + void testFromJsonDoubleToInteger() throws Exception { + Object value = coder.decode("20", Object.class); + assertEquals(Integer.valueOf(20), value); + } + + @Test + void testStandardTypeAdapter() { + String yaml = "abc: def\n"; + StandardCoderObject sco = coder.fromJson(yaml, StandardCoderObject.class); + assertNotNull(sco.getData()); + assertEquals("{'abc':'def'}".replace('\'', '"'), sco.getData().toString()); + assertEquals(yaml, coder.toJson(sco)); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java new file mode 100644 index 000000000..563181c02 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java @@ -0,0 +1,171 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import lombok.EqualsAndHashCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.error.YAMLException; + +class YamlJsonTranslatorTest { + private static final File YAML_FILE = + new File("src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml"); + + private Container cont; + private YamlJsonTranslator translator; + + /** + * Creates {@link #translator} and uses it to load {@link #cont}. + * + * @throws IOException if an error occurs + */ + @BeforeEach + public void setUp() throws IOException { + translator = new YamlJsonTranslator(); + + try (FileReader rdr = new FileReader(YAML_FILE)) { + cont = translator.fromYaml(rdr, Container.class); + } + } + + @Test + void testToYamlObject() { + String yaml = translator.toYaml(cont); + + Container cont2 = translator.fromYaml(yaml, Container.class); + assertEquals(cont, cont2); + } + + @Test + void testToYamlWriterObject() throws IOException { + IOException ex = new IOException("expected exception"); + + // writer that throws an exception when the write() method is invoked + Writer wtr = new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + throw ex; + } + + @Override + public void flush() throws IOException { + // do nothing + } + + @Override + public void close() throws IOException { + // do nothing + } + }; + + assertThatThrownBy(() -> translator.toYaml(wtr, cont)).isInstanceOf(YAMLException.class); + + wtr.close(); + } + + @Test + void testFromYamlStringClassOfT() throws IOException { + String yaml = new String(Files.readAllBytes(YAML_FILE.toPath()), StandardCharsets.UTF_8); + Container cont2 = translator.fromYaml(yaml, Container.class); + assertEquals(cont, cont2); + } + + @Test + void testFromYamlReaderClassOfT() { + verify(cont); + } + + /** + * Verifies that the container has contents matching the yaml file. + * + * @param container container whose contents are to be verified + */ + public static void verify(Container container) { + assertNotNull(container.item); + assertTrue(container.item.boolVal); + assertEquals(1000L, container.item.longVal); + assertEquals(1010.1f, container.item.floatVal, 0.00001); + + assertEquals(4, container.list.size()); + assertNull(container.list.get(1)); + + assertEquals(20, container.list.get(0).intVal); + assertEquals("string 30", container.list.get(0).stringVal); + assertNull(container.list.get(0).nullVal); + + assertEquals(40.0, container.list.get(2).doubleVal, 0.000001); + assertNull(container.list.get(2).nullVal); + assertNotNull(container.list.get(2).another); + assertEquals(50, container.list.get(2).another.intVal); + + assertTrue(container.list.get(3).boolVal); + + assertNotNull(container.map); + assertEquals(3, container.map.size()); + + assertNotNull(container.map.get("itemA")); + assertEquals("stringA", container.map.get("itemA").stringVal); + + assertNotNull(container.map.get("itemB")); + assertEquals("stringB", container.map.get("itemB").stringVal); + + double dbl = 123456789012345678901234567890.0; + assertEquals(dbl, container.map.get("itemB").doubleVal, 1000.0); + + assertNotNull(container.map.get("itemC")); + assertTrue(container.map.get("itemC").boolVal); + } + + + @EqualsAndHashCode + public static class Container { + protected Item item; + protected List list; + protected Map map; + } + + @EqualsAndHashCode + public static class Item { + protected boolean boolVal; + protected int intVal; + protected long longVal; + protected double doubleVal; + protected float floatVal; + protected String stringVal; + protected Object nullVal; + protected Item another; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonSerializerTest.java new file mode 100644 index 000000000..aa701435a --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonSerializerTest.java @@ -0,0 +1,48 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.gson; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import org.junit.jupiter.api.Test; + +class GsonSerializerTest { + + @Test + void testReadJsonReader() { + JsonReader rdr = new JsonReader(new StringReader("10")); + + GsonSerializer ser = new GsonSerializer() { + @Override + public void write(JsonWriter out, Object value) throws IOException { + // do nothing + } + }; + + assertThatThrownBy(() -> ser.read(rdr)).isInstanceOf(UnsupportedOperationException.class) + .hasMessage("read from pseudo TypeAdapter"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilderTest.java new file mode 100644 index 000000000..d85653d94 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilderTest.java @@ -0,0 +1,92 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GsonTestUtilsBuilderTest { + + private GsonTestUtils utils; + + @BeforeEach + public void setUp() { + utils = new MyBuilder().build(); + } + + @Test + void testBuilderAddMock() { + PreMock pre = mock(PreMock.class); + when(pre.getId()).thenReturn(2000); + + assertEquals("{\"name\":2000}", utils.gsonEncode(pre)); + } + + /** + * Builder that provides an adapter for mock(PreMock.class). + */ + private static class MyBuilder extends GsonTestUtilsBuilder { + public MyBuilder() { + TypeAdapterFactory sgson = new TypeAdapterFactory() { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class clazz = type.getRawType(); + + if (PreMock.class.isAssignableFrom(clazz)) { + return new GsonSerializer() { + @Override + public void write(JsonWriter out, T value) throws IOException { + PreMock obj = (PreMock) value; + out.beginObject().name("name").value(obj.getId()).endObject(); + } + }; + } + + return null; + } + }; + + addMock(PreMock.class, sgson); + } + } + + /** + * Class that will be mocked. + */ + public static class PreMock { + protected int id = 1000; + + public int getId() { + return id; + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsTest.java new file mode 100644 index 000000000..d355823c1 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsTest.java @@ -0,0 +1,235 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.gson; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GsonTestUtilsTest { + private static final String HELLO = "hello"; + + private GsonTestUtils utils; + + @BeforeEach + public void setUp() { + utils = new GsonTestUtils(); + } + + @Test + void testGetGson() { + assertNotNull(utils.getGson()); + } + + @Test + void testGsonRoundTrip() { + Data data = new Data(); + data.setId(500); + + // try with null text + data.setText(null); + assertEquals(data.toString(), utils.gsonRoundTrip(data, Data.class).toString()); + + // try with non-null text + data.setText(HELLO); + assertEquals(data.toString(), utils.gsonRoundTrip(data, Data.class).toString()); + } + + @Test + void testCompareGsonObjectClass_testCompareGsonObjectFile() { + Data data = new Data(); + data.setId(500); + data.setText(HELLO); + + utils.compareGson(data, GsonTestUtilsTest.class); + + // file not found + File file = new File(GsonTestUtilsTest.class.getSimpleName() + "-NotFound.json"); + + assertThatThrownBy(() -> utils.compareGson(data, file)) + .isInstanceOf(JsonParseException.class) + .hasCauseInstanceOf(FileNotFoundException.class); + + // force I/O error while reading file + GsonTestUtils utils2 = new GsonTestUtils() { + @Override + protected String readFile(File file) throws IOException { + throw new IOException("expected exception"); + } + }; + assertThatThrownBy(() -> utils2.compareGson(data, GsonTestUtilsTest.class)) + .isInstanceOf(JsonParseException.class).hasCauseInstanceOf(IOException.class) + .hasMessage("error reading: GsonTestUtilsTest.json"); + } + + @Test + void testCompareGsonObjectString() { + Data data = new Data(); + data.setId(600); + data.setText(HELLO); + + assertThatCode(() -> utils.compareGson(data, "{'id': ${obj.id}, 'text': '${obj.text}'}".replace('\'', '"'))) + .doesNotThrowAnyException(); + } + + @Test + void testCompareGsonObjectJsonElement() { + Data data = new Data(); + data.setId(650); + data.setText(HELLO); + + JsonObject json = new JsonObject(); + json.addProperty("id", data.getId()); + json.addProperty("text", data.getText()); + + utils.compareGson(data, json); + + // mismatch + data.setText("world"); + assertThatThrownBy(() -> utils.compareGson(data, json)).isInstanceOf(AssertionError.class); + } + + @Test + void testApplyScripts() { + Data data = new Data(); + data.setId(700); + data.setText(HELLO); + + String result = utils.applyScripts("no interpolation", data); + assertEquals("no interpolation", result); + + result = utils.applyScripts("${obj.id} at start, ${obj.text} in middle, and end ${obj.id}", data); + assertEquals("700 at start, hello in middle, and end 700", result); + + // try null value + data.setText(null); + result = utils.applyScripts("use ${obj.text} this", data); + assertEquals("use null this", result); + assertEquals("use null this", utils.applyScripts("use ${obj.text} this", null)); + } + + @Test + void testReorderJsonObject() { + // insert properties in a non-alphabetical order + JsonObject inner = new JsonObject(); + inner.addProperty("objBint", 100); + inner.add("objBNull", JsonNull.INSTANCE); + inner.addProperty("objB", true); + + JsonArray arr = new JsonArray(); + arr.add(110); + arr.add(inner); + arr.add(false); + + JsonObject outer = new JsonObject(); + outer.add("objANull", JsonNull.INSTANCE); + outer.addProperty("objA", true); + outer.addProperty("objAStr", "obj-a-string"); + outer.add("nested-array", arr); + + outer = utils.reorder(outer); + assertEquals("{'nested-array':[110,{'objB':true,'objBint':100},false],'objA':true,'objAStr':'obj-a-string'}" + .replace('\'', '"'), outer.toString()); + } + + @Test + void testReorderJsonArray() { + // insert properties in a non-alphabetical order + JsonObject inner = new JsonObject(); + inner.add("objCNull", JsonNull.INSTANCE); + inner.addProperty("objCStr", "obj-c-string"); + inner.addProperty("objC", true); + + JsonArray arr = new JsonArray(); + arr.add(200); + arr.add(inner); + arr.add(false); + + arr = utils.reorder(arr); + assertEquals("[200,{'objC':true,'objCStr':'obj-c-string'},false]".replace('\'', '"'), arr.toString()); + } + + @Test + void testReorderJsonElement() { + // null element + JsonElement jsonEl = null; + assertNull(utils.reorder(jsonEl)); + + // object element + JsonObject obj = new JsonObject(); + obj.add("objDNull", JsonNull.INSTANCE); + obj.addProperty("objDStr", "obj-d-string"); + obj.addProperty("objD", true); + jsonEl = obj; + jsonEl = utils.reorder(jsonEl); + assertEquals("{'objD':true,'objDStr':'obj-d-string'}".replace('\'', '"'), jsonEl.toString()); + + // boolean + jsonEl = obj.get("objD"); + jsonEl = utils.reorder(jsonEl); + assertEquals("true", jsonEl.toString()); + + // JsonNull + jsonEl = JsonNull.INSTANCE; + jsonEl = utils.reorder(jsonEl); + assertEquals("null", jsonEl.toString()); + + // array element + JsonObject inner = new JsonObject(); + inner.add("objENull", JsonNull.INSTANCE); + inner.addProperty("objEStr", "obj-e-string"); + inner.addProperty("objE", true); + + JsonArray arr = new JsonArray(); + arr.add(300); + arr.add(inner); + arr.add(false); + jsonEl = arr; + jsonEl = utils.reorder(jsonEl); + assertEquals("[300,{'objE':true,'objEStr':'obj-e-string'},false]".replace('\'', '"'), jsonEl.toString()); + } + + @Setter + @Getter + @ToString + public static class Data { + private int id; + private String text; + + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/network/NetworkUtilTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/network/NetworkUtilTest.java new file mode 100644 index 000000000..ccd67fc49 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/network/NetworkUtilTest.java @@ -0,0 +1,127 @@ +/* + * ============LICENSE_START======================================================= + * policy-utils + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.network; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class NetworkUtilTest { + protected static Logger logger = LoggerFactory.getLogger(NetworkUtilTest.class); + + private static final String LOCALHOST = "localhost"; + + @Test + void test() throws InterruptedException, IOException { + assertNotNull(NetworkUtil.IPV4_WILDCARD_ADDRESS); + assertFalse(NetworkUtil.isTcpPortOpen(LOCALHOST, NetworkUtil.allocPort(), 1, 5)); + assertNotNull(NetworkUtil.getHostname()); + assertNotNull(NetworkUtil.getHostIp()); + } + + @Test + void testAlwaysTrustManager() throws Exception { + TrustManager[] mgrarr = NetworkUtil.getAlwaysTrustingManager(); + assertEquals(1, mgrarr.length); + assertInstanceOf(X509TrustManager.class, mgrarr[0]); + + X509TrustManager mgr = (X509TrustManager) mgrarr[0]; + assertNotNull(mgr.getAcceptedIssuers()); + assertEquals(0, mgr.getAcceptedIssuers().length); + + // these should not throw exceptions + mgr.checkClientTrusted(null, null); + mgr.checkServerTrusted(null, null); + } + + @Test + void testAllocPort_testAllocPortString__testAllocPortInetSocketAddress() throws Exception { + // allocate wild-card port + int wildCardPort = NetworkUtil.allocPort(); + assertNotEquals(0, wildCardPort); + + // verify that we can listen on the port + try (ServerSocket wildSocket = new ServerSocket(wildCardPort)) { + new Accepter(wildSocket).start(); + assertTrue(NetworkUtil.isTcpPortOpen(LOCALHOST, wildCardPort, 5, 1000L)); + } + + + // allocate port using host name + int localPort = NetworkUtil.allocPort(LOCALHOST); + assertNotEquals(0, localPort); + + // the OS should have allocated a new port, even though the first has been closed + assertNotEquals(wildCardPort, localPort); + + try (ServerSocket localSocket = new ServerSocket()) { + localSocket.bind(new InetSocketAddress(LOCALHOST, localPort)); + new Accepter(localSocket).start(); + assertTrue(NetworkUtil.isTcpPortOpen(LOCALHOST, localPort, 5, 1000L)); + } + } + + @Test + void testGenUniqueName() { + String name = NetworkUtil.genUniqueName(LOCALHOST); + assertThat(name).isNotBlank().isNotEqualTo(LOCALHOST); + + // second call should generate a different value + assertThat(NetworkUtil.genUniqueName(LOCALHOST)).isNotEqualTo(name); + } + + /** + * Thread that accepts a connection on a socket. + */ + private static class Accepter extends Thread { + private ServerSocket socket; + + public Accepter(ServerSocket socket) { + this.socket = socket; + setDaemon(true); + } + + @Override + public void run() { + try (Socket server = socket.accept()) { //NOSONAR + // do nothing + + } catch (IOException e) { + logger.error("socket not accepted", e); + } + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/properties/PropertyUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/properties/PropertyUtilsTest.java new file mode 100644 index 000000000..a8b37f574 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/properties/PropertyUtilsTest.java @@ -0,0 +1,110 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Properties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PropertyUtilsTest { + private static final String DFLT_STRING = "my-default"; + private static final int DLFT_INT = 1000; + + private PropertyUtils utils; + private String invalidName; + private String invalidValue; + private Exception invalidEx; + + /** + * Initializes {@link #utils}. + */ + @BeforeEach + public void setUp() { + Properties properties = new Properties(); + properties.put("myPrefix.my-string", "some text"); + properties.put("myPrefix.empty-string", ""); + + properties.put("myPrefix.my-bool", "true"); + properties.put("myPrefix.my-bool2", "false"); + properties.put("myPrefix.empty-bool", ""); + properties.put("myPrefix.invalid-bool", "not a bool"); + + properties.put("myPrefix.my-int", "100"); + properties.put("myPrefix.my-int2", "200"); + properties.put("myPrefix.empty-int", ""); + properties.put("myPrefix.invalid-int", "not an int"); + + utils = new PropertyUtils(properties, "myPrefix", (name, value, ex) -> { + invalidName = name; + invalidValue = value; + invalidEx = ex; + }); + } + + @Test + void testGetString() { + assertEquals("some text", utils.getString(".my-string", DFLT_STRING)); + assertEquals(DFLT_STRING, utils.getString(".empty-string", DFLT_STRING)); + assertEquals(DFLT_STRING, utils.getString(".missing-string", DFLT_STRING)); + + assertNull(invalidName); + assertNull(invalidValue); + assertNull(invalidEx); + } + + @Test + void testGetBoolean() { + assertTrue(utils.getBoolean(".my-bool", false)); + assertFalse(utils.getBoolean(".my-bool2", true)); + assertTrue(utils.getBoolean(".empty-bool", true)); + assertFalse(utils.getBoolean(".invalid-bool", true)); + assertTrue(utils.getBoolean(".missing-bool", true)); + + assertNull(invalidName); + assertNull(invalidValue); + assertNull(invalidEx); + } + + @Test + void testGetInteger() { + assertEquals(100, utils.getInteger(".my-int", DLFT_INT)); + assertEquals(200, utils.getInteger(".my-int2", DLFT_INT)); + assertEquals(DLFT_INT, utils.getInteger(".empty-int", DLFT_INT)); + assertEquals(DLFT_INT, utils.getInteger(".missing-int", DLFT_INT)); + + assertNull(invalidName); + assertNull(invalidValue); + assertNull(invalidEx); + + assertEquals(DLFT_INT, utils.getInteger(".invalid-int", DLFT_INT)); + + assertEquals("myPrefix.invalid-int", invalidName); + assertEquals("not an int", invalidValue); + assertTrue(invalidEx instanceof NumberFormatException); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java new file mode 100644 index 000000000..7b41d9765 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java @@ -0,0 +1,337 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020-2021, 2023-2024 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.resources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * The Class ResourceUtilsTest. + * + * @author Liam Fallon (liam.fallon@ericsson.com) + */ +class ResourceUtilsTest { + private File tmpDir = null; + private File tmpEmptyFile = null; + private File tmpUsedFile = null; + + private String jarDirResource = null; + private String jarFileResource = null; + + private static final String RESOURCES_PATH = "src/test/resources/"; + private static final String PATH_DIR_RESOURCE = "testdir"; + private static final String PATH_FILE_RESOURCE = "testdir/testfile.xml"; + + private static final String NON_EXISTENT_RESOURCE = "somewhere/over/the/rainbow"; + private static final String INVALID_RESOURCE = "@%%%\\\\_:::DESD"; + + /** + * Setup resource utils test. + * + * @throws IOException Signals that an I/O exception has occurred. + */ + @BeforeEach + public void setupResourceUtilsTest() throws IOException { + tmpDir = new File(System.getProperty("java.io.tmpdir")); + tmpEmptyFile = File.createTempFile(this.getClass().getName(), ".tmp"); + tmpUsedFile = File.createTempFile(this.getClass().getName(), ".tmp"); + + jarDirResource = "META-INF"; + jarFileResource = "META-INF/MANIFEST.MF"; + + try (final FileWriter fileWriter = new FileWriter(tmpUsedFile)) { + fileWriter.write("Bluebirds fly over the rainbow"); + } + } + + /** + * Clean resource utils test. + */ + @AfterEach + public void cleanDownResourceUtilsTest() { + assertTrue(tmpEmptyFile.delete()); + assertTrue(tmpUsedFile.delete()); + } + + /** + * Test get url resource. + */ + @Test + void testgetUrlResource() { + URL theUrl = ResourceUtils.getUrlResource(tmpDir.getAbsolutePath()); + assertNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(tmpEmptyFile.getAbsolutePath()); + assertNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(tmpUsedFile.getAbsolutePath()); + assertNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(jarDirResource); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(jarFileResource); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(PATH_DIR_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(PATH_FILE_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrlResource("file:///" + PATH_DIR_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_DIR_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_FILE_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(NON_EXISTENT_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(INVALID_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getUrlResource(null); + assertNull(theUrl); + } + + /** + * Test get local file. + */ + @Test + void testGetLocalFile() { + URL theUrl = ResourceUtils.getLocalFile(tmpDir.getAbsolutePath()); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(tmpEmptyFile.getAbsolutePath()); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(tmpUsedFile.getAbsolutePath()); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(jarDirResource); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(jarFileResource); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(PATH_DIR_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(PATH_FILE_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_DIR_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_FILE_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(NON_EXISTENT_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(INVALID_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile("file:///"); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getLocalFile("file:///testdir/testfile.xml"); + assertNull(theUrl); + + theUrl = ResourceUtils.getLocalFile(null); + assertNull(theUrl); + } + + /** + * Test get resource as stream. + */ + @Test + void testGetResourceAsStream() throws IOException { + verifyStream(tmpDir.getAbsolutePath()); + verifyStream(tmpEmptyFile.getAbsolutePath()); + verifyStream(tmpUsedFile.getAbsolutePath()); + verifyStream(jarDirResource); + verifyStream(jarFileResource); + verifyStream(PATH_DIR_RESOURCE); + verifyStream(PATH_FILE_RESOURCE); + verifyStream(RESOURCES_PATH + PATH_DIR_RESOURCE); + verifyStream(RESOURCES_PATH + PATH_FILE_RESOURCE); + assertNull(ResourceUtils.getResourceAsStream(NON_EXISTENT_RESOURCE)); + assertNull(ResourceUtils.getResourceAsStream(INVALID_RESOURCE)); + assertNull(ResourceUtils.getResourceAsStream(null)); + verifyStream(""); + } + + private void verifyStream(String path) throws IOException { + try (var theStream = ResourceUtils.getResourceAsStream(path)) { + assertNotNull(theStream); + } + } + + /** + * Test get resource as string. + */ + @Test + void testGetResourceAsString() { + String theString = ResourceUtils.getResourceAsString(tmpDir.getAbsolutePath()); + assertNotNull(theString); + + theString = ResourceUtils.getResourceAsString(tmpEmptyFile.getAbsolutePath()); + assertEquals("", theString); + + theString = ResourceUtils.getResourceAsString(tmpUsedFile.getAbsolutePath()); + assertEquals("Bluebirds fly over the rainbow", theString); + + theString = ResourceUtils.getResourceAsString(jarFileResource); + assertNotNull(theString); + + theString = ResourceUtils.getResourceAsString(PATH_DIR_RESOURCE); + assertNotNull(theString); + + theString = ResourceUtils.getResourceAsString(PATH_FILE_RESOURCE); + assertNotNull(theString); + + theString = ResourceUtils.getResourceAsString(RESOURCES_PATH + PATH_DIR_RESOURCE); + assertNotNull(theString); + + theString = ResourceUtils.getResourceAsString(RESOURCES_PATH + PATH_FILE_RESOURCE); + assertNotNull(theString); + + theString = ResourceUtils.getResourceAsString(NON_EXISTENT_RESOURCE); + assertNull(theString); + + theString = ResourceUtils.getResourceAsString(INVALID_RESOURCE); + assertNull(theString); + + theString = ResourceUtils.getResourceAsString(null); + assertNull(theString); + + theString = ResourceUtils.getResourceAsString(""); + + assertEquals("keystore-test\nlogback-test.xml\nMETA-INF\norg\ntestdir\nversion.txt\nwebapps\n", theString); + + } + + @Test + void testgetUrl4Resource() { + URL theUrl = ResourceUtils.getUrl4Resource(tmpDir.getAbsolutePath()); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(tmpEmptyFile.getAbsolutePath()); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(tmpUsedFile.getAbsolutePath()); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(jarDirResource); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(jarFileResource); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(PATH_DIR_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(PATH_FILE_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(RESOURCES_PATH + PATH_DIR_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(RESOURCES_PATH + PATH_FILE_RESOURCE); + assertNotNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(NON_EXISTENT_RESOURCE); + assertNull(theUrl); + + theUrl = ResourceUtils.getUrl4Resource(INVALID_RESOURCE); + assertNull(theUrl); + } + + @Test + void testGetFilePath4Resource() { + assertNull(ResourceUtils.getFilePath4Resource(null)); + assertEquals("/something/else", ResourceUtils.getFilePath4Resource("/something/else")); + assertTrue(ResourceUtils.getFilePath4Resource("xml/example.xml").endsWith("xml/example.xml")); + assertTrue(ResourceUtils.getFilePath4Resource("com/google").contains("com/google")); + } + + @Test + void testGetDirectoryContents() throws MalformedURLException { + assertTrue(ResourceUtils.getDirectoryContents(null).isEmpty()); + assertTrue(ResourceUtils.getDirectoryContents("idontexist").isEmpty()); + assertTrue(ResourceUtils.getDirectoryContents("logback-test.xml").isEmpty()); + + Set resultD0 = ResourceUtils.getDirectoryContents("testdir"); + assertEquals(1, resultD0.size()); + assertEquals("testdir/testfile.xml", normalizePath(resultD0.iterator().next())); + + Set resultD1 = ResourceUtils.getDirectoryContents("org/onap/policy/common/utils"); + assertFalse(resultD1.isEmpty()); + assertEquals("org/onap/policy/common/utils/coder/", normalizePath(resultD1.iterator().next())); + + Set resultD2 = ResourceUtils.getDirectoryContents("org/onap/policy/common/utils/coder"); + assertTrue(resultD2.size() >= 15); + assertEquals("org/onap/policy/common/utils/coder/CoderExceptionTest.class", + normalizePath(resultD2.iterator().next())); + + Set resultJ0 = ResourceUtils.getDirectoryContents("com"); + assertTrue(resultJ0.contains("com/google/")); + assertEquals("com/google/", normalizePath(resultJ0.iterator().next())); + + Set resultJ1 = ResourceUtils.getDirectoryContents("com/google/gson"); + assertTrue(resultJ1.size() > 1); + assertTrue(resultJ1.contains("com/google/gson/JsonElement.class")); + + URL dummyUrl = new URL("http://even/worse"); + assertTrue(ResourceUtils.getDirectoryContentsJar(dummyUrl, "nonexistantdirectory").isEmpty()); + + } + + /** + * Normalizes a path name, replacing OS-specific separators with "/". + * + * @param pathName path name to be normalized + * @return the normalized path name + */ + private String normalizePath(String pathName) { + return pathName.replace(File.separator, "/"); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/FeatureApiUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/FeatureApiUtilsTest.java new file mode 100644 index 000000000..3de937795 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/services/FeatureApiUtilsTest.java @@ -0,0 +1,83 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Predicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FeatureApiUtilsTest { + private static final String HANDLED = "handled"; + + private MyPred pred; + private List tried; + private List errors; + + /** + * Initializes fields. + */ + @BeforeEach + public void setUp() { + tried = new LinkedList<>(); + errors = new LinkedList<>(); + pred = new MyPred(); + } + + @Test + void testApplyFeatureTrue() { + assertTrue(FeatureApiUtils.apply(Arrays.asList("exceptT0", "falseT1", HANDLED, "falseT2", HANDLED), pred, + (str, ex) -> errors.add(str))); + + assertEquals("[exceptT0, falseT1, handled]", tried.toString()); + assertEquals("[exceptT0]", errors.toString()); + } + + @Test + void testApplyFeatureFalse() { + List lst = Arrays.asList("falseF1", "exceptF2", "falseF3"); + + assertFalse(FeatureApiUtils.apply(lst, pred, (str, ex) -> errors.add(str))); + assertEquals(lst.toString(), tried.toString()); + assertEquals("[exceptF2]", errors.toString()); + } + + private class MyPred implements Predicate { + + @Override + public boolean test(String data) { + tried.add(data); + + if (data.startsWith("except")) { + throw new IllegalArgumentException("expected exception"); + } + + return data.equals(HANDLED); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceImplTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceImplTest.java new file mode 100644 index 000000000..1508009fa --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceImplTest.java @@ -0,0 +1,206 @@ +/* + * ============LICENSE_START======================================================= + * utils + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OrderedServiceImplTest { + + private static final int HIGH_PRIORITY_NUM = -1000; + private static final int LOW_PRIORITY_NUM = 1000; + + private static GenericService highPrioService; + private static GenericService lowPrioService; + + /** + * Saves the original state of the ordered service list to restore after each test. + */ + @BeforeAll + public static void setup() { + List implementers = GenericService.providers.getList(); + highPrioService = implementers.get(0); + lowPrioService = implementers.get(1); + } + + /** + * Restores original state after each test. + */ + @BeforeEach + public void resetOrder() { + highPrioService.setSequenceNumber(HIGH_PRIORITY_NUM); + lowPrioService.setSequenceNumber(LOW_PRIORITY_NUM); + } + + /** + * Tests obtaining a list of service implementers. + */ + @Test + void getListTest() { + List implementers = GenericService.providers.getList(); + assertEquals(2, implementers.size()); + + assertEquals(highPrioService, implementers.get(0)); + assertEquals(HIGH_PRIORITY_NUM, highPrioService.getSequenceNumber()); + + assertEquals(lowPrioService, implementers.get(1)); + assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber()); + } + + /** + * Tests inverting the priority of two services to ensure the list is rebuilt + * with the new order. + */ + @Test + void rebuildListInvertedPriorityTest() { + + List implementers = GenericService.providers.getList(); + assertEquals(2, implementers.size()); + + assertEquals(highPrioService, implementers.get(0)); + assertEquals(HIGH_PRIORITY_NUM, highPrioService.getSequenceNumber()); + + assertEquals(lowPrioService, implementers.get(1)); + assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber()); + + highPrioService.setSequenceNumber(LOW_PRIORITY_NUM); + lowPrioService.setSequenceNumber(HIGH_PRIORITY_NUM); + + implementers = GenericService.providers.rebuildList(); + assertEquals(2, implementers.size()); + + assertEquals(lowPrioService, implementers.get(0)); + assertEquals(HIGH_PRIORITY_NUM, lowPrioService.getSequenceNumber()); + + assertEquals(highPrioService, implementers.get(1)); + assertEquals(LOW_PRIORITY_NUM, highPrioService.getSequenceNumber()); + + } + + /** + * Tests that the service list is ordered alphabetically by class names + * if the priorities are equivalent. + */ + @Test + void rebuildListEqualPriorityTest() { + + List implementers = GenericService.providers.getList(); + assertEquals(2, implementers.size()); + + assertEquals(highPrioService, implementers.get(0)); + assertEquals(HIGH_PRIORITY_NUM, highPrioService.getSequenceNumber()); + + assertEquals(lowPrioService, implementers.get(1)); + assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber()); + + highPrioService.setSequenceNumber(LOW_PRIORITY_NUM); + lowPrioService.setSequenceNumber(LOW_PRIORITY_NUM); + + implementers = GenericService.providers.rebuildList(); + assertEquals(2, implementers.size()); + + assertEquals(highPrioService, implementers.get(0)); + assertEquals(LOW_PRIORITY_NUM, highPrioService.getSequenceNumber()); + + assertEquals(lowPrioService, implementers.get(1)); + assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber()); + + } + + /** + * Test interface that extends OrderedService to allow changing the sequence number. + */ + public static interface GenericService extends OrderedService { + + /** + * Providers of the GenericService interface. + */ + OrderedServiceImpl providers = new OrderedServiceImpl<>(GenericService.class); + + /** + * Sets the sequence number of the service. + */ + public void setSequenceNumber(int seqNum); + + } + + /** + * A high priority service class. + */ + public static class HighPriorityService implements GenericService { + + /** + * Defaults to a high priority. + */ + private int seqNum = HIGH_PRIORITY_NUM; + + /** + * {@inheritDoc}. + */ + @Override + public int getSequenceNumber() { + return this.seqNum; + } + + /** + * {@inheritDoc}. + */ + @Override + public void setSequenceNumber(int seqNum) { + this.seqNum = seqNum; + } + + } + + /** + * A low priority service class. + */ + public static class LowPriorityService implements GenericService { + + /** + * Defaults to a low priority. + */ + private int seqNum = LOW_PRIORITY_NUM; + + /** + * {@inheritDoc}. + */ + @Override + public int getSequenceNumber() { + return this.seqNum; + } + + /** + * {@inheritDoc}. + */ + @Override + public void setSequenceNumber(int seqNum) { + this.seqNum = seqNum; + } + + } + +} \ No newline at end of file diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceTest.java new file mode 100644 index 000000000..adac7cb03 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceTest.java @@ -0,0 +1,62 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class OrderedServiceTest { + + @Test + void testGetSequenceNumber() { + // Anonymous class implementation for testing + OrderedService service = () -> 5; // Returns 5 as the sequence number + + // Test getSequenceNumber + assertEquals(5, service.getSequenceNumber(), "The sequence number should be 5"); + } + + @Test + void testGetName() { + // Anonymous class implementation for testing + OrderedService service = () -> 5; + + // Test getName + assertEquals(service.getClass().getName(), service.getName(), "The name should match the class name"); + } + + @Test + void testGetNameWithCustomImplementation() { + // Custom implementation of OrderedService + class CustomOrderedService implements OrderedService { + @Override + public int getSequenceNumber() { + return 10; + } + } + + OrderedService service = new CustomOrderedService(); + + // Test getName for custom implementation + assertEquals(service.getClass().getName(), service.getName(), + "The name should match the custom implementation class name"); + } +} + diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerContainerTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerContainerTest.java new file mode 100644 index 000000000..1bae46cb1 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerContainerTest.java @@ -0,0 +1,116 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx; + +class ServiceManagerContainerTest { + private static final String MY_NAME = "my-name"; + private static final String MY_ACTION = "my-action"; + private static final String MY_OBJECT = "my-object"; + private RunnableWithEx starter; + private RunnableWithEx stopper; + private Startable startObj; + private MyCont cont; + + /** + * Set up. + */ + @BeforeEach + public void setUp() { + starter = mock(RunnableWithEx.class); + stopper = mock(RunnableWithEx.class); + startObj = mock(Startable.class); + + cont = new MyCont(MY_NAME); + } + + @Test + void testServiceManagerContainer() throws Exception { + // use no-arg constructor + cont = new MyCont(); + assertEquals("service manager", cont.getName()); + + cont.start(); + verify(starter).run(); + } + + @Test + void test() throws Exception { + assertEquals(MY_NAME, cont.getName()); + + assertFalse(cont.isAlive()); + + cont.start(); + assertTrue(cont.isAlive()); + verify(starter).run(); + verify(startObj).start(); + verify(stopper, never()).run(); + verify(startObj, never()).stop(); + verify(startObj, never()).shutdown(); + + cont.stop(); + assertFalse(cont.isAlive()); + verify(starter).run(); + verify(startObj).start(); + verify(stopper).run(); + verify(startObj).stop(); + verify(startObj, never()).shutdown(); + } + + @Test + void testShutdown() throws Exception { + cont.start(); + cont.shutdown(); + assertFalse(cont.isAlive()); + verify(stopper).run(); + verify(startObj).stop(); + verify(startObj, never()).shutdown(); + } + + private class MyCont extends ServiceManagerContainer { + + public MyCont() { + addServices(); + } + + public MyCont(String name) { + super(name); + addServices(); + } + + private void addServices() { + addAction(MY_ACTION, starter, stopper); + addService(MY_OBJECT, startObj); + } + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java new file mode 100644 index 000000000..8d6c8a280 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +class ServiceManagerExceptionTest { + private ServiceManagerException sme; + + @Test + void testServiceManagerException() { + sme = new ServiceManagerException(); + assertNull(sme.getMessage()); + assertNull(sme.getCause()); + } + + @Test + void testServiceManagerExceptionString() { + sme = new ServiceManagerException("hello"); + assertEquals("hello", sme.getMessage()); + assertNull(sme.getCause()); + } + + @Test + void testServiceManagerExceptionThrowable() { + Throwable thrown = new Throwable("expected exception"); + sme = new ServiceManagerException(thrown); + assertNotNull(sme.getMessage()); + assertSame(thrown, sme.getCause()); + } + + @Test + void testServiceManagerExceptionStringThrowable() { + Throwable thrown = new Throwable("another expected exception"); + sme = new ServiceManagerException("world", thrown); + assertEquals("world", sme.getMessage()); + assertSame(thrown, sme.getCause()); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java new file mode 100644 index 000000000..dc5555842 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java @@ -0,0 +1,296 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx; + +class ServiceManagerTest { + private static final String MY_NAME = "my-name"; + private static final String ALREADY_RUNNING = MY_NAME + " is already running"; + private static final String EXPECTED_EXCEPTION = "expected exception"; + + private ServiceManager svcmgr; + + /** + * Initializes {@link #svcmgr}. + */ + @BeforeEach + public void setUp() { + svcmgr = new ServiceManager(MY_NAME); + } + + @Test + void testServiceName() { + assertEquals("service manager", new ServiceManager().getName()); + } + + @Test + void testGetName() { + assertEquals(MY_NAME, svcmgr.getName()); + } + + @Test + void testAddAction() throws Exception { + RunnableWithEx start1 = mock(RunnableWithEx.class); + RunnableWithEx stop1 = mock(RunnableWithEx.class); + svcmgr.addAction("first action", start1, stop1); + + RunnableWithEx start2 = mock(RunnableWithEx.class); + RunnableWithEx stop2 = mock(RunnableWithEx.class); + svcmgr.addAction("second action", start2, stop2); + + svcmgr.start(); + verify(start1).run(); + verify(start2).run(); + verify(stop1, never()).run(); + verify(stop2, never()).run(); + + // cannot add while running + assertThatIllegalStateException().isThrownBy(() -> svcmgr.addAction("fail action", start1, stop1)) + .withMessage(ALREADY_RUNNING + "; cannot add fail action"); + + svcmgr.stop(); + verify(start1).run(); + verify(start2).run(); + verify(stop1).run(); + verify(stop2).run(); + } + + @Test + void testAddStartable() { + Startable start1 = mock(Startable.class); + svcmgr.addService("first startable", start1); + + Startable start2 = mock(Startable.class); + svcmgr.addService("second startable", start2); + + svcmgr.start(); + verify(start1).start(); + verify(start1, never()).stop(); + verify(start2).start(); + verify(start2, never()).stop(); + + // cannot add while running + assertThatIllegalStateException().isThrownBy(() -> svcmgr.addService("fail startable", start1)) + .withMessage(ALREADY_RUNNING + "; cannot add fail startable"); + + svcmgr.stop(); + verify(start1).start(); + verify(start1).stop(); + verify(start2).start(); + verify(start2).stop(); + } + + @Test + void testStart() { + Startable start1 = mock(Startable.class); + svcmgr.addService("test start", start1); + + assertTrue(svcmgr.start()); + + assertTrue(svcmgr.isAlive()); + verify(start1).start(); + verify(start1, never()).stop(); + + // cannot re-start + assertThatIllegalStateException().isThrownBy(() -> svcmgr.start()).withMessage(ALREADY_RUNNING); + + // verify that it didn't try to start the service again + verify(start1).start(); + + // still running + assertTrue(svcmgr.isAlive()); + } + + @Test + void testStart_Ex() { + Startable start1 = mock(Startable.class); + svcmgr.addService("test start ex", start1); + + Startable start2 = mock(Startable.class); + svcmgr.addService("second test start ex", start2); + + // this one will throw an exception + Startable start3 = mock(Startable.class); + RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION); + when(start3.start()).thenThrow(exception); + svcmgr.addService("third test start ex", start3); + + Startable start4 = mock(Startable.class); + svcmgr.addService("fourth test start ex", start4); + + Startable start5 = mock(Startable.class); + svcmgr.addService("fifth test start ex", start5); + + assertThatThrownBy(() -> svcmgr.start()).isInstanceOf(ServiceManagerException.class).hasCause(exception); + + assertFalse(svcmgr.isAlive()); + + verify(start1).start(); + verify(start2).start(); + verify(start3).start(); + verify(start4, never()).start(); + verify(start5, never()).start(); + + verify(start1).stop(); + verify(start2).stop(); + verify(start3, never()).stop(); + verify(start4, never()).stop(); + verify(start5, never()).stop(); + } + + @Test + void testStart_RewindEx() { + Startable start1 = mock(Startable.class); + svcmgr.addService("test start rewind", start1); + + // this one will throw an exception during rewind + Startable start2 = mock(Startable.class); + RuntimeException exception2 = new RuntimeException(EXPECTED_EXCEPTION); + when(start2.stop()).thenThrow(exception2); + svcmgr.addService("second test start rewind", start2); + + // this one will throw an exception + Startable start3 = mock(Startable.class); + RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION); + when(start3.start()).thenThrow(exception); + svcmgr.addService("third test start rewind", start3); + + Startable start4 = mock(Startable.class); + svcmgr.addService("fourth test start rewind", start4); + + Startable start5 = mock(Startable.class); + svcmgr.addService("fifth test start rewind", start5); + + assertThatThrownBy(() -> svcmgr.start()).isInstanceOf(ServiceManagerException.class).hasCause(exception); + + assertFalse(svcmgr.isAlive()); + } + + @Test + void testStop() { + Startable start1 = mock(Startable.class); + svcmgr.addService("first stop", start1); + + // cannot stop until started + assertThatIllegalStateException().isThrownBy(() -> svcmgr.stop()).withMessage(MY_NAME + " is not running"); + + // verify that it didn't try to stop the service + verify(start1, never()).stop(); + + // start it + svcmgr.start(); + + assertTrue(svcmgr.stop()); + + assertFalse(svcmgr.isAlive()); + verify(start1).stop(); + } + + @Test + void testStop_Ex() throws Exception { + RunnableWithEx start1 = mock(RunnableWithEx.class); + RunnableWithEx stop1 = mock(RunnableWithEx.class); + svcmgr.addAction("first stop ex", start1, stop1); + + Startable start2 = mock(Startable.class); + svcmgr.addService("second stop ex", start2); + + svcmgr.start(); + verify(start1).run(); + verify(stop1, never()).run(); + verify(start2).start(); + verify(start2, never()).stop(); + + svcmgr.stop(); + verify(start1).run(); + verify(stop1).run(); + verify(start2).start(); + verify(start2).stop(); + + assertFalse(svcmgr.isAlive()); + } + + @Test + void testShutdown() { + Startable start1 = mock(Startable.class); + svcmgr.addService("first stop", start1); + + // cannot stop until started + assertThatIllegalStateException().isThrownBy(() -> svcmgr.shutdown()).withMessage(MY_NAME + " is not running"); + + // verify that it didn't try to stop the service + verify(start1, never()).stop(); + + // start it + svcmgr.start(); + + svcmgr.shutdown(); + + assertFalse(svcmgr.isAlive()); + verify(start1).stop(); + } + + @Test + void testRewind() { + RunnableWithEx starter = mock(RunnableWithEx.class); + LinkedList lst = new LinkedList<>(); + + svcmgr.addAction("first rewind", starter, () -> lst.add("rewind1")); + svcmgr.addAction("second rewind", starter, () -> lst.add("rewind2")); + + // this one will throw an exception during rewind + RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION); + svcmgr.addAction("third rewind", starter, () -> { + lst.add("rewind3"); + throw exception; + }); + + svcmgr.addAction("fourth rewind", starter, () -> lst.add("rewind4")); + svcmgr.addAction("fifth rewind", starter, () -> lst.add("rewind5")); + + svcmgr.start(); + + assertThatThrownBy(() -> svcmgr.stop()).isInstanceOf(ServiceManagerException.class).hasCause(exception); + + assertFalse(svcmgr.isAlive()); + + // all of them should have been stopped, in reverse order + assertEquals(Arrays.asList("rewind5", "rewind4", "rewind3", "rewind2", "rewind1").toString(), lst.toString()); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ConstructionErrorTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ConstructionErrorTest.java new file mode 100644 index 000000000..95bba10de --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/test/ConstructionErrorTest.java @@ -0,0 +1,35 @@ +/* + * ============LICENSE_START======================================================= + * Common Utils-Test + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ConstructionErrorTest extends ErrorsTester { + + @Test + void test() { + assertEquals(4, testAllError(ConstructionError.class)); + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ErrorsTesterTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ErrorsTesterTest.java new file mode 100644 index 000000000..b6d91f07e --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/test/ErrorsTesterTest.java @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START======================================================= + * Common Utils-Test + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ErrorsTesterTest { + + @Test + void test() { + assertEquals(2, new ErrorsTester().testAllError(SimpleError.class)); + assertEquals(5, new ErrorsTester().testAllError(StaticError.class)); + } + + /** + * Used to test a simple success case. + */ + public static class SimpleError extends Error { + private static final long serialVersionUID = 1L; + + public SimpleError() { + super(); + } + + public SimpleError(String message) { + super(message); + } + } + + /** + * Used to test the exhaustive success case. + */ + public static class StaticError extends Error { + private static final long serialVersionUID = 1L; + + public StaticError() { + super(); + } + + public StaticError(String message) { + super(message); + } + + public StaticError(Throwable cause) { + super(cause); + } + + public StaticError(String message, Throwable cause) { + super(message, cause); + } + + public StaticError(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ExceptionsTesterTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ExceptionsTesterTest.java new file mode 100644 index 000000000..81a850354 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/test/ExceptionsTesterTest.java @@ -0,0 +1,112 @@ +/* + * ============LICENSE_START======================================================= + * Common Utils-Test + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ExceptionsTesterTest { + + @Test + void test() { + assertEquals(2, new ExceptionsTester().test(SimpleException.class)); + assertEquals(8, new ExceptionsTester().test(StaticException.class)); + } + + @Test + void testNoConstructorsException() { + ExceptionsTester tester = new ExceptionsTester(); + assertThatThrownBy(() -> tester.test(NoConstructorsException.class)) + .isInstanceOf(AssertionError.class); + } + + /** + * Used to test a failure case - this has no standard constructors. The only constructor it has + * takes an "int", thus it is not one of the standard constructors. + */ + public static class NoConstructorsException extends Exception { + private static final long serialVersionUID = 1L; + + public NoConstructorsException(int value) { + super(); + } + } + + /** + * Used to test a simple success case. + */ + public static class SimpleException extends Exception { + private static final long serialVersionUID = 1L; + + public SimpleException() { + super(); + } + + public SimpleException(String message) { + super(message); + } + } + + /** + * Used to test the exhaustive success case. + */ + public static class StaticException extends Exception { + private static final long serialVersionUID = 1L; + + public StaticException() { + super(); + } + + public StaticException(String message) { + super(message); + } + + public StaticException(Throwable cause) { + super(cause); + } + + public StaticException(String message, Throwable cause) { + super(message, cause); + } + + public StaticException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + // same as above, but with Exceptions substituted for Throwables + + public StaticException(Exception cause) { + super(cause); + } + + public StaticException(String message, Exception cause) { + super(message, cause); + } + + public StaticException(String message, Exception cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ThrowablesTesterTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ThrowablesTesterTest.java new file mode 100644 index 000000000..7775ef35b --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/test/ThrowablesTesterTest.java @@ -0,0 +1,230 @@ +/* + * ============LICENSE_START======================================================= + * Common Utils-Test + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ThrowablesTesterTest { + + @Test + void test() { + assertEquals(2, new ThrowablesTester().testAllThrowable(SimpleThrowable.class)); + assertEquals(5, new ThrowablesTester().testAllThrowable(StaticThrowable.class)); + } + + @Test + void testNoConstructorsThrowable() { + // this will not throw an error, but it should return 0, as there are + // no matching constructors + assertEquals(0, new ThrowablesTester().testAllThrowable(NoConstructorsThrowable.class)); + } + + @Test + void testIgnoreMessageThrowable() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(IgnoreMessageThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + @Test + void testIgnoreCauseThrowable() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(IgnoreCauseThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + @Test + void testAlwaysSuppressThrowable() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(AlwaysSuppressThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + @Test + void testNeverSuppressThrowable() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(NeverSuppressThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + @Test + void testAlwaysWritableThrowable() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(AlwaysWritableThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + @Test + void testNeverWritableThrowable() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(NeverWritableThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + @Test + void testThrowInstantiationException() { + ThrowablesTester tester = new ThrowablesTester(); + assertThatThrownBy(() -> tester.testAllThrowable(ThrowInstantiationThrowable.class)) + .isInstanceOf(AssertionError.class); + } + + /** + * Used to test a failure case - message text is ignored. + */ + public static class IgnoreMessageThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public IgnoreMessageThrowable(String message) { + super("bogus"); + } + } + + /** + * Used to test a failure case - cause is ignored. + */ + public static class IgnoreCauseThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public IgnoreCauseThrowable(Throwable cause) { + super(new Throwable("another cause")); + } + } + + /** + * Used to test a failure case - this has no standard constructors. The only constructor it has + * takes an "int", thus it is not one of the standard constructors. + */ + public static class NoConstructorsThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public NoConstructorsThrowable(int value) { + super(); + } + } + + /** + * Used to test a failure case - always suppresses. + */ + public static class AlwaysSuppressThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public AlwaysSuppressThrowable(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, true, writableStackTrace); + } + } + + /** + * Used to test a failure case - never suppresses. + */ + public static class NeverSuppressThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public NeverSuppressThrowable(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, false, writableStackTrace); + } + } + + /** + * Used to test a failure case - always allows stack writes. + */ + public static class AlwaysWritableThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public AlwaysWritableThrowable(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, true); + } + } + + /** + * Used to test a failure case - never allows stack writes. + */ + public static class NeverWritableThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public NeverWritableThrowable(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, false); + } + } + + /** + * Used to test a failure case - throws InstantiationException when constructed. + */ + public static class ThrowInstantiationThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public ThrowInstantiationThrowable(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) throws InstantiationException { + + throw new InstantiationException(ThrowablesTester.EXPECTED_EXCEPTION_MSG); + } + } + + /** + * Used to test a simple success case. + */ + public static class SimpleThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public SimpleThrowable() { + super(); + } + + public SimpleThrowable(String message) { + super(message); + } + } + + /** + * Used to test the exhaustive success case. + */ + public static class StaticThrowable extends Throwable { + private static final long serialVersionUID = 1L; + + public StaticThrowable() { + super(); + } + + public StaticThrowable(String message) { + super(message); + } + + public StaticThrowable(Throwable cause) { + super(cause); + } + + public StaticThrowable(String message, Throwable cause) { + super(message, cause); + } + + public StaticThrowable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + } + +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/log/logback/ExtractAppenderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/log/logback/ExtractAppenderTest.java new file mode 100644 index 000000000..9367209cd --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/test/log/logback/ExtractAppenderTest.java @@ -0,0 +1,473 @@ +/* + * ============LICENSE_START======================================================= + * Common Utils-Test + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.test.log.logback; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class ExtractAppenderTest { + private static final String ABC_DIGIT = "abc[0-9]"; + private static final String ABC_DIGIT1 = "abc[1-9]"; + private static final String DEF_DIGIT = "def[0-9]"; + private static final String HELLO = "hello"; + private static final String HELLO_ABC = "hello abc"; + private static final String HELLO_ABC1_WORLD = "hello abc1 world"; + private static final String HELLO_ABC3 = "hello abc3"; + private static final String WORLD = "world"; + private static final String WORLD_ABC = "world abc"; + private static final String WORLD_GHI2_WORLD = "world ghi2 world"; + + /** + * Milliseconds to wait for a thread to terminate. + */ + private static final long THREAD_WAIT_MS = 5000L; + + private static Logger logger; + + private List threads; + + @BeforeAll + public static void setUpBeforeClass() { + logger = (Logger) LoggerFactory.getLogger(ExtractAppenderTest.class); + logger.setLevel(Level.INFO); + } + + @BeforeEach + public void setUp() { + threads = new LinkedList<>(); + } + + /** + * Tear down all appenders and threads. + */ + @AfterEach + public void tearDown() throws Exception { + logger.detachAndStopAllAppenders(); + + for (Thread p : threads) { + p.interrupt(); + p.join(THREAD_WAIT_MS); + } + } + + @Test + void testExtractAppender() { + AtomicInteger count = new AtomicInteger(0); + + ExtractAppender appender = new ExtractAppender() { + @Override + protected void append(ILoggingEvent event) { + count.incrementAndGet(); + super.append(event); + } + }; + + addAppender(appender); + + logger.info(HELLO); + logger.info(WORLD); + + // "append" should always be called + assertEquals(2, count.get()); + + // appender with no patterns - everything should match + assertEquals(strList(HELLO, WORLD), appender.getExtracted()); + + // add a pattern and verify match + appender.setPattern(ABC_DIGIT); + logger.info("hello abc1"); + + // this should not match + logger.info("hello def2"); + + assertEquals(4, count.get()); + assertEquals(strList(HELLO, WORLD, "abc1"), appender.getExtracted()); + } + + @Test + void testExtractAppenderStringArray() { + AtomicInteger count = new AtomicInteger(0); + + ExtractAppender appender = new ExtractAppender(ABC_DIGIT, DEF_DIGIT) { + @Override + protected void append(ILoggingEvent event) { + count.incrementAndGet(); + super.append(event); + } + }; + + addAppender(appender); + + logger.info(HELLO_ABC1_WORLD); + logger.info(WORLD_GHI2_WORLD); // no match + logger.info("world def3 world"); + logger.info("hello abc4"); + logger.info("abc5 world"); + logger.info("hello def6"); + logger.info("ghi7 world"); // no match + logger.info("def8 world"); + + // "append" should always be called + assertEquals(8, count.get()); + + assertEquals(strList("abc1", "def3", "abc4", "abc5", "def6", "def8"), appender.getExtracted()); + + appender.setPattern("ghi[0-9]"); + logger.info("hello abc9"); + logger.info("hello ghi9"); + + // this should not match + logger.info("hello xyz"); + + assertEquals(11, count.get()); + assertEquals(strList("abc1", "def3", "abc4", "abc5", "def6", "def8", "abc9", "ghi9"), appender.getExtracted()); + } + + @Test + void testExtractAppenderQueueStringArray() { + // no. of matches allowed in the list + int nallowed = 3; + + AtomicInteger count = new AtomicInteger(0); + + LinkedList queue = new LinkedList() { + private static final long serialVersionUID = 1L; + + @Override + public boolean offer(String element) { + if (count.incrementAndGet() <= nallowed) { + return super.offer(element); + + } else { + return false; + } + } + }; + + ExtractAppender appender = new ExtractAppender(queue, ABC_DIGIT); + addAppender(appender); + + // these shouldn't match + for (int x = 0; x < 10; ++x) { + logger.info("xyz"); + } + + int nmatches = 10; + + LinkedList expected = new LinkedList<>(); + + for (int x = 0; x < nmatches; ++x) { + String msg = "abc" + x; + logger.info("{} world", msg); + + if (x < nallowed) { + expected.add(msg); + } + } + + // "offer" should always be called for a match + assertEquals(nmatches, count.get()); + + assertEquals(expected, appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_NoPatterns() { + ExtractAppender appender = makeAppender(); + + logger.info(HELLO); + logger.info(WORLD); + + assertEquals(strList(HELLO, WORLD), appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_Formatted() { + ExtractAppender appender = makeAppender(); + + logger.info("hello {} world{}", "there", "!"); + + assertEquals(strList("hello there world!"), appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_MatchFirstPattern() { + ExtractAppender appender = makeAppender(ABC_DIGIT, DEF_DIGIT); + + logger.info("hello abc1"); + logger.info("world xyz2"); + + assertEquals(strList("abc1"), appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_MatchLastPattern() { + ExtractAppender appender = makeAppender(ABC_DIGIT, DEF_DIGIT); + + logger.info("hello def1"); + logger.info("world xyz2"); + + assertEquals(strList("def1"), appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_Group1() { + ExtractAppender appender = makeAppender("hello (abc)|(xyz)", DEF_DIGIT); + + logger.info("hello abc, world!"); + logger.info(WORLD_ABC); + + assertEquals(strList("abc"), appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_Group3() { + ExtractAppender appender = makeAppender("hello (abc)|(pdq)|(xyz)", DEF_DIGIT); + + logger.info("say hello xyz, world!"); + logger.info(WORLD_ABC); + + assertEquals(strList("xyz"), appender.getExtracted()); + } + + @Test + void testAppendILoggingEvent_NoGroup() { + ExtractAppender appender = makeAppender(HELLO_ABC); + + logger.info("say hello abc, world!"); + logger.info(WORLD_ABC); + + assertEquals(strList(HELLO_ABC), appender.getExtracted()); + } + + @Test + void testGetExtracted() { + ExtractAppender appender = makeAppender(ABC_DIGIT1); + + logger.info(HELLO_ABC1_WORLD); + logger.info(WORLD_GHI2_WORLD); // no match + logger.info(HELLO_ABC3); + + List oldlst = appender.getExtracted(); + assertEquals(strList("abc1", "abc3"), oldlst); + assertEquals(oldlst, appender.getExtracted()); + + logger.info("abc9"); + assertEquals(strList("abc1", "abc3", "abc9"), appender.getExtracted()); + } + + @Test + void testClearExtractions() { + final ExtractAppender appender = makeAppender(ABC_DIGIT1); + + logger.info(HELLO_ABC1_WORLD); + logger.info(WORLD_GHI2_WORLD); + logger.info(HELLO_ABC3); + + assertEquals(strList("abc1", "abc3"), appender.getExtracted()); + + appender.clearExtractions(); + + // list should be empty now + assertEquals(strList(), appender.getExtracted()); + + logger.info("hello abc4 world"); + logger.info("world ghi5 world"); + logger.info("hello abc6"); + + // list should only contain the new items + assertEquals(strList("abc4", "abc6"), appender.getExtracted()); + } + + @Test + void testSetPattern() { + final ExtractAppender appender = makeAppender(ABC_DIGIT1); + + logger.info(HELLO_ABC1_WORLD); + logger.info(WORLD_GHI2_WORLD); // no match + logger.info(HELLO_ABC3); + + assertEquals(strList("abc1", "abc3"), appender.getExtracted()); + + appender.setPattern("ghi[0-9]"); + + logger.info("world ghi4 world"); // this should match now + logger.info("hello abc5"); // this should still match + logger.info("hello xyz5"); // no match + + assertEquals(strList("abc1", "abc3", "ghi4", "abc5"), appender.getExtracted()); + } + + /** + * Launches threads doing everything in parallel to ensure nothing crashes. + */ + @Test + void test_MultiThreaded() throws Exception { + // when to stop + long tend = System.currentTimeMillis() + 250; + + // maximum number of items allowed in the extraction list + int maxItems = 10; + + // this will be set if one of the threads generates an error + AtomicBoolean err = new AtomicBoolean(false); + + // extracted messages go here - this is a finite-length queue since + // we don't know how many messages may actually be logged + LinkedList queue = new LinkedList() { + private static final long serialVersionUID = 1L; + + @Override + public boolean offer(String element) { + if (size() < maxItems) { + return super.offer(element); + } else { + return false; + } + } + }; + + ExtractAppender app = new ExtractAppender(queue, ABC_DIGIT1); + addAppender(app); + + // create some threads to add another pattern + addThread(tend, err, xtxt -> app.setPattern(DEF_DIGIT)); + + // create some threads to log "abc" messages + addThread(tend, err, xtxt -> logger.info("{}{}world!", HELLO_ABC, xtxt)); + + // create some threads to log "def" messages + addThread(tend, err, xtxt -> logger.info("hello def{}world!", xtxt)); + + // create some threads to get extractions + addThread(tend, err, xtxt -> app.getExtracted()); + + /* + * Finally ready to start. + */ + + // start all of the threads + for (Thread t : threads) { + t.setDaemon(true); + t.start(); + } + + // wait for each thread to stop + for (Thread t : threads) { + t.join(THREAD_WAIT_MS); + assertFalse(t.isAlive()); + } + + // ensure none of the threads threw an exception + assertFalse(err.get()); + } + + /** + * Adds multiple threads to perform some function repeatedly until the given time is reached. + * + * @param tend time, in milliseconds, when the test should terminate + * @param haderr this will be set to {@code true} if the function throws an exception other than + * an InterruptedException + * @param func function to be repeatedly invoked + */ + private void addThread(long tend, AtomicBoolean haderr, VoidFunction func) { + // number of threads of each type to create + int neach = 3; + + for (int x = 0; x < neach; ++x) { + String xtxt = String.valueOf(x); + + threads.add(new Thread() { + @Override + public void run() { + try { + while (System.currentTimeMillis() < tend) { + func.apply(xtxt); + } + + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + + } catch (Exception ex) { + haderr.set(true); + } + } + }); + + } + } + + /** + * Makes an appender that recognizes the given set of strings. + * + * @param strings regular expressions to be matched + * @return a new appender + */ + private ExtractAppender makeAppender(String... strings) { + ExtractAppender appender = new ExtractAppender(strings); + + addAppender(appender); + + return appender; + } + + /** + * Adds an appender to the logger. + * + * @param app appender to be added + */ + private void addAppender(ExtractAppender app) { + app.setContext(logger.getLoggerContext()); + app.start(); + + logger.addAppender(app); + } + + /** + * Converts an array of strings into a list of strings. + * + * @param strings array of strings + * @return a list of the strings + */ + private List strList(String... strings) { + return Arrays.asList(strings); + } + + @FunctionalInterface + public interface VoidFunction { + public void apply(String text) throws InterruptedException; + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/validation/AssertionsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/validation/AssertionsTest.java new file mode 100644 index 000000000..b633425f2 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/validation/AssertionsTest.java @@ -0,0 +1,95 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2016-2018 Ericsson. All rights reserved. + * Modifications Copyright (C) 2019-2024 Nordix Foundation. + * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.validation; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +/** + * The Class ResourceUtilsTest. + * + * @author Liam Fallon (liam.fallon@ericsson.com) + */ +class AssertionsTest { + private static final String HELLO = "Hello"; + private static final String IT_IS_OK = "it is OK"; + private static final String IT_IS_NULL = "it is null"; + private static final String IT_IS_TRUE = "it is true"; + private static final String IT_IS_FALSE = "it is false"; + + @Test + void testAssertions() { + Assertions.argumentNotFalse(true, IT_IS_TRUE); + + assertThatIllegalArgumentException().isThrownBy(() -> Assertions.argumentNotFalse(false, IT_IS_FALSE)) + .withMessage(IT_IS_FALSE); + + + Assertions.argumentOfClassNotFalse(true, ArithmeticException.class, IT_IS_TRUE); + + assertThatIllegalArgumentException().isThrownBy( + () -> Assertions.argumentOfClassNotFalse(false, ArithmeticException.class, IT_IS_FALSE)) + .withMessage(IT_IS_FALSE); + + + Assertions.argumentNotNull(HELLO, IT_IS_OK); + + assertThatIllegalArgumentException().isThrownBy(() -> Assertions.argumentNotNull(null, IT_IS_NULL)) + .withMessage(IT_IS_NULL); + + + Assertions.argumentOfClassNotNull(true, ArithmeticException.class, IT_IS_OK); + + assertThatIllegalArgumentException().isThrownBy( + () -> Assertions.argumentOfClassNotNull(null, ArithmeticException.class, IT_IS_NULL)) + .withMessage(IT_IS_NULL); + + + Assertions.assignableFrom(java.util.TreeMap.class, java.util.Map.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Assertions.assignableFrom(java.util.Map.class, java.util.TreeMap.class)) + .withMessage("java.util.Map is not an instance of java.util.TreeMap"); + + + Assertions.instanceOf(HELLO, String.class); + + assertThatIllegalArgumentException().isThrownBy(() -> Assertions.instanceOf(100, String.class)) + .withMessage("java.lang.Integer is not an instance of java.lang.String"); + + + Assertions.validateStringParameter("name", "MyName", "^M.*e$"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Assertions.validateStringParameter("name", "MyName", "^M.*f$")) + .withMessage("parameter \"name\": value \"MyName\", does not match regular expression \"^M.*f$\""); + + + assertNull(Assertions.getStringParameterValidationMessage("Greeting", HELLO, "^H.*o$")); + assertEquals("parameter Greeting with value Hello does not match regular expression Goodbye", + Assertions.getStringParameterValidationMessage("Greeting", HELLO, "Goodbye")); + } +} diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/validation/VersionTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/validation/VersionTest.java new file mode 100644 index 000000000..190231242 --- /dev/null +++ b/policy-common/src/test/java/org/onap/policy/common/utils/validation/VersionTest.java @@ -0,0 +1,155 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019-2024 Nordix Foundation. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.validation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VersionTest { + private static final String TYPE = "my-type"; + private static final String NAME = "my-name"; + + private static final int MAJOR = 10; + private static final int MINOR = 2; + private static final int PATCH = 3; + + private Version vers; + + @BeforeEach + public void setUp() { + vers = new Version(MAJOR, MINOR, PATCH); + } + + @Test + void testHashCode() { + int hash = vers.hashCode(); + int hash2 = new Version(MAJOR, MINOR, PATCH + 1).hashCode(); + assertNotEquals(hash, hash2); + } + + @Test + void testConstructor() { + Version versionTest = new Version("1.0.2"); + assertEquals(1, versionTest.getMajor()); + assertEquals(0, versionTest.getMinor()); + assertEquals(2, versionTest.getPatch()); + + versionTest = new Version("null"); + assertEquals(0, versionTest.getMajor()); + assertEquals(0, versionTest.getMinor()); + assertEquals(0, versionTest.getPatch()); + } + + @Test + void testMakeVersion() { + assertEquals("9.8.7", Version.makeVersion(TYPE, NAME, "9.8.7").toString()); + assertEquals("9.0.0", Version.makeVersion(TYPE, NAME, "9").toString()); + + assertNull(Version.makeVersion(TYPE, NAME, "")); + assertNull(Version.makeVersion(TYPE, NAME, "a.3.4")); + assertNull(Version.makeVersion(TYPE, NAME, "100.")); + assertNull(Version.makeVersion(TYPE, NAME, "10000000000000000.2.3")); + assertNull(Version.makeVersion(TYPE, NAME, "1.20000000000000000.3")); + assertNull(Version.makeVersion(TYPE, NAME, "1.2.30000000000000000")); + } + + @Test + void testNewVersion() { + vers = vers.newVersion(); + assertEquals("11.0.0", vers.toString()); + } + + @Test + void testEquals() { + assertNotEquals(null, vers); + assertNotEquals(vers, new Object()); + + assertEquals(vers, vers); + + assertEquals(vers, new Version(MAJOR, MINOR, PATCH)); + + assertNotEquals(vers, new Version(MAJOR + 1, MINOR, PATCH)); + assertNotEquals(vers, new Version(MAJOR, MINOR + 1, PATCH)); + assertNotEquals(vers, new Version(MAJOR, MINOR, PATCH + 1)); + } + + @Test + void testCompareTo() { + vers = new Version(101, 201, 301); + + // equals case + assertEquals(0, new Version(101, 201, 301).compareTo(vers)); + + // major takes precedence + assertTrue(new Version(102, 200, 300).compareTo(vers) > 0); + + // minor takes precedence over patch + assertTrue(new Version(101, 202, 300).compareTo(vers) > 0); + + // compare major + assertTrue(new Version(100, 201, 301).compareTo(vers) < 0); + assertTrue(new Version(102, 201, 301).compareTo(vers) > 0); + + // compare minor + assertTrue(new Version(101, 200, 301).compareTo(vers) < 0); + assertTrue(new Version(101, 202, 301).compareTo(vers) > 0); + + // compare patch + assertTrue(new Version(101, 201, 300).compareTo(vers) < 0); + assertTrue(new Version(101, 201, 302).compareTo(vers) > 0); + } + + @Test + void testToString() { + assertEquals("10.2.3", vers.toString()); + } + + @Test + void testGetMajor() { + assertEquals(MAJOR, vers.getMajor()); + } + + @Test + void testGetMinor() { + assertEquals(MINOR, vers.getMinor()); + } + + @Test + void testGetPatch() { + assertEquals(PATCH, vers.getPatch()); + } + + @Test + void testVersionIntIntInt() { + assertEquals("5.6.7", new Version(5, 6, 7).toString()); + } + + @Test + void testVersion() { + assertEquals("0.0.0", new Version().toString()); + } +} diff --git a/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi b/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi new file mode 100644 index 000000000..7889cb8f0 --- /dev/null +++ b/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi @@ -0,0 +1 @@ +org.onap.policy.common.message.bus.utils.NetLoggerUtilTest$NetLoggerFeature \ No newline at end of file diff --git a/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.utils.services.OrderedServiceImplTest$GenericService b/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.utils.services.OrderedServiceImplTest$GenericService new file mode 100644 index 000000000..1e920f465 --- /dev/null +++ b/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.utils.services.OrderedServiceImplTest$GenericService @@ -0,0 +1,2 @@ +org.onap.policy.common.utils.services.OrderedServiceImplTest$HighPriorityService +org.onap.policy.common.utils.services.OrderedServiceImplTest$LowPriorityService \ No newline at end of file diff --git a/policy-common/src/test/resources/keystore-test b/policy-common/src/test/resources/keystore-test new file mode 100644 index 000000000..5820e0f0c Binary files /dev/null and b/policy-common/src/test/resources/keystore-test differ diff --git a/policy-common/src/test/resources/logback-test.xml b/policy-common/src/test/resources/logback-test.xml new file mode 100644 index 000000000..ddf4f8bbf --- /dev/null +++ b/policy-common/src/test/resources/logback-test.xml @@ -0,0 +1,42 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M\(%line\) - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/http/server/internal/HttpServerTest.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/http/server/internal/HttpServerTest.json new file mode 100644 index 000000000..aba9d682f --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/http/server/internal/HttpServerTest.json @@ -0,0 +1,8 @@ +{ + "alive": false, + "host": "localhost", + "name": "echo", + "port": ${obj.port}, + "sniHostCheck":false, + "prometheus":false +} diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_invalid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_invalid.json new file mode 100644 index 000000000..a7455525d --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_invalid.json @@ -0,0 +1,6 @@ +{ + "port": 6969, + "userName": "username", + "password": "password", + "https": true +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_valid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_valid.json new file mode 100644 index 000000000..61d793a83 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_valid.json @@ -0,0 +1,7 @@ +{ + "host": "0.0.0.0", + "port": 6969, + "userName": "username", + "password": "password", + "https": true +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_all_params.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_all_params.json new file mode 100644 index 000000000..89e464dd3 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_all_params.json @@ -0,0 +1,62 @@ +{ + "topicSources" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic" : "my-effective-topic", + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "port": 123, + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "consumer group", + "consumerInstance" : "consumer instance", + "fetchTimeout" : 15000, + "fetchLimit" : 100, + "userName": "username", + "password": "password", + "managed": true, + "environment": "environment1", + "aftEnvironment": "aftEnvironment1", + "partner": "partner1", + "latitude": "1234", + "longitude": "1234", + "partitionId": "partition_id", + "additionalProps": {"xyz":"xyz"}, + "clientName": "clientName1", + "hostname": "hostname1", + "basePath": "basePath1", + "serializationProvider": "serializationProvider1" + }], + "topicSinks" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic" : "my-effective-topic", + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "port": 123, + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "consumer group", + "consumerInstance" : "consumer instance", + "fetchTimeout" : 15000, + "fetchLimit" : 100, + "userName": "username", + "password": "password", + "managed": true, + "environment": "environment1", + "aftEnvironment": "aftEnvironment1", + "partner": "partner1", + "latitude": "1234", + "longitude": "1234", + "partitionId": "partition_id", + "additionalProps": {"xyz":"xyz"}, + "clientName": "clientName1", + "hostname": "hostname1", + "basePath": "basePath1", + "serializationProvider": "serializationProvider1" + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_invalid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_invalid.json new file mode 100644 index 000000000..775b4886d --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_invalid.json @@ -0,0 +1,6 @@ +{ + "topicSources" : [{ + "topic" : "ueb-source", + "servers" : ["my-server"] + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_missing_mandatory.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_missing_mandatory.json new file mode 100644 index 000000000..216c11ec8 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_missing_mandatory.json @@ -0,0 +1,12 @@ +{ + "topicSources" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [], + "topicCommInfrastructure" : "kafka" + }], + "topicSinks" : [ { + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_valid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_valid.json new file mode 100644 index 000000000..2603bfdcd --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_valid.json @@ -0,0 +1,30 @@ +{ + "topicSources" : [ { + "topic" : "ueb-source", + "servers" : [ "my-server" ], + "topicCommInfrastructure" : "ueb" + },{ + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + },{ + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka" + }], + "topicSinks" : [ { + "topic" : "ueb-sink", + "servers" : [ "my-server" ], + "topicCommInfrastructure" : "ueb" + },{ + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + },{ + "topic" : "policy-pdp-pap3", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic":"effectiveTopic1", + "allowSelfSignedCerts":true + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json new file mode 100644 index 000000000..5b2e712ab --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json @@ -0,0 +1,30 @@ +{ + "locked": false, + "alive": false, + "topicSources": [ + { + "servers": [ + "my-server" + ], + "topic": "noop-source", + "effectiveTopic": "noop-source", + "recentEvents": [], + "alive": false, + "locked": false, + "topicCommInfrastructure": "NOOP" + } + ], + "topicSinks": [ + { + "servers": [ + "my-server" + ], + "topic": "noop-sink", + "effectiveTopic": "noop-sink", + "recentEvents": [], + "alive": false, + "locked": false, + "topicCommInfrastructure": "NOOP" + } + ] +} diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json new file mode 100644 index 000000000..462278a4a --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json @@ -0,0 +1,14 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-effective-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "topicCommInfrastructure" : "NOOP" +} diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json new file mode 100644 index 000000000..1f2fb55fd --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json @@ -0,0 +1,15 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-effective-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "topicCommInfrastructure" : "NOOP", + "partitionKey" : "my-partition" +} diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json new file mode 100644 index 000000000..305620c86 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json @@ -0,0 +1,18 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-effective-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "my-cons-group", + "consumerInstance" : "my-cons-inst", + "fetchTimeout" : 101, + "fetchLimit" : 100, + "topicCommInfrastructure" : "NOOP" +} diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json new file mode 100644 index 000000000..b72b4efd5 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json @@ -0,0 +1,9 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "topicCommInfrastructure" : "NOOP" +} diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json new file mode 100644 index 000000000..89e464dd3 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json @@ -0,0 +1,62 @@ +{ + "topicSources" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic" : "my-effective-topic", + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "port": 123, + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "consumer group", + "consumerInstance" : "consumer instance", + "fetchTimeout" : 15000, + "fetchLimit" : 100, + "userName": "username", + "password": "password", + "managed": true, + "environment": "environment1", + "aftEnvironment": "aftEnvironment1", + "partner": "partner1", + "latitude": "1234", + "longitude": "1234", + "partitionId": "partition_id", + "additionalProps": {"xyz":"xyz"}, + "clientName": "clientName1", + "hostname": "hostname1", + "basePath": "basePath1", + "serializationProvider": "serializationProvider1" + }], + "topicSinks" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic" : "my-effective-topic", + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "port": 123, + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "consumer group", + "consumerInstance" : "consumer instance", + "fetchTimeout" : 15000, + "fetchLimit" : 100, + "userName": "username", + "password": "password", + "managed": true, + "environment": "environment1", + "aftEnvironment": "aftEnvironment1", + "partner": "partner1", + "latitude": "1234", + "longitude": "1234", + "partitionId": "partition_id", + "additionalProps": {"xyz":"xyz"}, + "clientName": "clientName1", + "hostname": "hostname1", + "basePath": "basePath1", + "serializationProvider": "serializationProvider1" + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json new file mode 100644 index 000000000..775b4886d --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json @@ -0,0 +1,6 @@ +{ + "topicSources" : [{ + "topic" : "ueb-source", + "servers" : ["my-server"] + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json new file mode 100644 index 000000000..216c11ec8 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json @@ -0,0 +1,12 @@ +{ + "topicSources" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [], + "topicCommInfrastructure" : "kafka" + }], + "topicSinks" : [ { + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json new file mode 100644 index 000000000..2603bfdcd --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json @@ -0,0 +1,30 @@ +{ + "topicSources" : [ { + "topic" : "ueb-source", + "servers" : [ "my-server" ], + "topicCommInfrastructure" : "ueb" + },{ + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + },{ + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka" + }], + "topicSinks" : [ { + "topic" : "ueb-sink", + "servers" : [ "my-server" ], + "topicCommInfrastructure" : "ueb" + },{ + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + },{ + "topic" : "policy-pdp-pap3", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic":"effectiveTopic1", + "allowSelfSignedCerts":true + }] +} \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/utils/coder/StandardCoder.json b/policy-common/src/test/resources/org/onap/policy/common/utils/coder/StandardCoder.json new file mode 100644 index 000000000..b50b53b44 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/utils/coder/StandardCoder.json @@ -0,0 +1 @@ +[3000,3010] \ No newline at end of file diff --git a/policy-common/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml b/policy-common/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml new file mode 100644 index 000000000..1da7bfa3a --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml @@ -0,0 +1,21 @@ +--- +item: &top + boolVal: true + longVal: 1000 + floatVal: 1010.1 +list: +- intVal: 20 + stringVal: string 30 + nullVal: +- +- doubleVal: 40 + another: + intVal: 50 +- *top +map: + itemA: + stringVal: stringA + itemB: + stringVal: stringB + doubleVal: 123456789012345678901234567890 + itemC: *top diff --git a/policy-common/src/test/resources/org/onap/policy/common/utils/gson/GsonTestUtilsTest.json b/policy-common/src/test/resources/org/onap/policy/common/utils/gson/GsonTestUtilsTest.json new file mode 100644 index 000000000..ff7ed70b1 --- /dev/null +++ b/policy-common/src/test/resources/org/onap/policy/common/utils/gson/GsonTestUtilsTest.json @@ -0,0 +1,4 @@ +{ + "id": ${obj.id}, + "text": "${obj.text}" +} diff --git a/policy-common/src/test/resources/testdir/testfile.xml b/policy-common/src/test/resources/testdir/testfile.xml new file mode 100644 index 000000000..4de3db14c --- /dev/null +++ b/policy-common/src/test/resources/testdir/testfile.xml @@ -0,0 +1,20 @@ + + diff --git a/policy-common/src/test/resources/version.txt b/policy-common/src/test/resources/version.txt new file mode 100644 index 000000000..9970c7b49 --- /dev/null +++ b/policy-common/src/test/resources/version.txt @@ -0,0 +1 @@ +ONAP Version test. diff --git a/policy-common/src/test/resources/webapps/alt-root/index.html b/policy-common/src/test/resources/webapps/alt-root/index.html new file mode 100644 index 000000000..8ef757e62 --- /dev/null +++ b/policy-common/src/test/resources/webapps/alt-root/index.html @@ -0,0 +1,30 @@ + + + + + + Hello World + + +

Test Jetty Static Resources Alt-Root

+ + diff --git a/policy-common/src/test/resources/webapps/root/index.html b/policy-common/src/test/resources/webapps/root/index.html new file mode 100644 index 000000000..35f610135 --- /dev/null +++ b/policy-common/src/test/resources/webapps/root/index.html @@ -0,0 +1,30 @@ + + + + + + Hello World + + +

Test Jetty Static Resources Root

+ + diff --git a/policy-models/pom.xml b/policy-models/pom.xml index 9823bba8b..f525714b1 100644 --- a/policy-models/pom.xml +++ b/policy-models/pom.xml @@ -35,20 +35,9 @@ - org.onap.policy.common - common-parameters - ${policy.common.version} - - - org.onap.policy.common - utils - ${policy.common.version} - - - org.onap.policy.common - utils-test - ${policy.common.version} - test + org.onap.policy.clamp + policy-common + ${project.version} com.google.code.gson diff --git a/pom.xml b/pom.xml index 51d02a3f1..91a073a7b 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ This is the root Maven module for the policy-clamp project. It contains the following modules: + - policy-common: dependency code previously in policy/common repo - policy-models: dependency code previously in policy/models repo - policy-clamp-common: Common code for all Policy/CLAMP modules - policy-clamp-models: POJOs and other model code for REST and Kafka messages and for persistence @@ -57,6 +58,7 @@ 5.0.0-SNAPSHOT + policy-common policy-models common examples diff --git a/runtime-acm/pom.xml b/runtime-acm/pom.xml index 6bc856b1d..a0d4d852e 100644 --- a/runtime-acm/pom.xml +++ b/runtime-acm/pom.xml @@ -43,37 +43,6 @@ policy-clamp-models ${project.version} - - org.onap.policy.common - common-parameters - ${policy.common.version} - - - org.onap.policy.common - gson - ${policy.common.version} - runtime - - - org.onap.policy.common - message-bus - ${policy.common.version} - - - org.onap.policy.common - policy-endpoints - ${policy.common.version} - - - org.onap.policy.common - spring-utils - ${policy.common.version} - - - org.onap.policy.common - utils - ${policy.common.version} - com.google.code.gson gson