diff --git a/asterisk-java-core/build.gradle b/asterisk-java-core/build.gradle new file mode 100644 index 00000000..b2b4104e --- /dev/null +++ b/asterisk-java-core/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java-library' +} + +java { + sourceCompatibility = '17' + targetCompatibility = '17' + + withSourcesJar() + withJavadocJar() +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.assertj:assertj-core:3.24.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testImplementation 'org.mockito:mockito-core:5.7.0' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/NewlineDelimiter.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/NewlineDelimiter.java new file mode 100644 index 00000000..42c928f4 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/NewlineDelimiter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core; + +/** + * Newline delimiters used for determine how lines was sent/received to/from Asterisk. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public enum NewlineDelimiter { + /** + * AGI uses LF (Line Feed) as a newline delimiter. + */ + LF("\n"), + + /** + * AMI uses CRLF (Carriage Return + Line Feed) as a newline delimiter. + */ + CRLF("\r\n"), + ; + + private final String pattern; + + NewlineDelimiter(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/AsteriskGenerator.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/AsteriskGenerator.java new file mode 100644 index 00000000..c899c6ec --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/AsteriskGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind; + +import org.asteriskjava.core.NewlineDelimiter; + +/** + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class AsteriskGenerator { + private static final String FIELD_NAME_VALUE_DELIMITER = ": "; + + private final StringBuilder stringBuilder = new StringBuilder(); + + private final NewlineDelimiter newlineDelimiter; + + public AsteriskGenerator(NewlineDelimiter newlineDelimiter) { + this.newlineDelimiter = newlineDelimiter; + } + + public void writeFieldName(String name) { + stringBuilder.append(name); + stringBuilder.append(FIELD_NAME_VALUE_DELIMITER); + } + + public void writeFieldValue(String value) { + stringBuilder.append(value); + stringBuilder.append(newlineDelimiter.getPattern()); + } + + public String generate() { + return stringBuilder.toString(); + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/AsteriskObjectMapper.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/AsteriskObjectMapper.java new file mode 100644 index 00000000..38634357 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/AsteriskObjectMapper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind; + +import org.asteriskjava.core.NewlineDelimiter; +import org.asteriskjava.core.databind.writer.AsteriskObjectMethodWriter; +import org.asteriskjava.core.databind.writer.AsteriskObjectWriter; + +import java.util.Comparator; +import java.util.List; + +import static java.util.Objects.requireNonNull; +import static org.asteriskjava.core.NewlineDelimiter.CRLF; +import static org.asteriskjava.core.NewlineDelimiter.LF; + +/** + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class AsteriskObjectMapper { + private final NewlineDelimiter newlineDelimiter; + private final Comparator fieldNamesComparator; + + private AsteriskObjectMapper( + NewlineDelimiter newlineDelimiter, + Comparator fieldNamesComparator + ) { + this.newlineDelimiter = newlineDelimiter; + this.fieldNamesComparator = fieldNamesComparator; + } + + public String writeValue(Object value) { + Class clazz = value.getClass(); + + AsteriskObjectWriter asteriskObjectWriter = new AsteriskObjectWriter(clazz, fieldNamesComparator); + + return writeValue(value, asteriskObjectWriter); + } + + private String writeValue(Object value, AsteriskObjectWriter asteriskObjectWriter) { + AsteriskGenerator asteriskGenerator = new AsteriskGenerator(newlineDelimiter); + List asteriskObjectMethodWriters = asteriskObjectWriter.getAsteriskObjectMethodWriters(); + for (AsteriskObjectMethodWriter asteriskObjectMethodWriter : asteriskObjectMethodWriters) { + asteriskObjectMethodWriter.writeName(asteriskGenerator); + asteriskObjectMethodWriter.writeValue(value, asteriskGenerator); + } + return asteriskGenerator.generate(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private NewlineDelimiter newlineDelimiter = CRLF; + private Comparator fieldNamesComparator; + + public Builder newlineDelimiter(NewlineDelimiter newlineDelimiter) { + this.newlineDelimiter = requireNonNull(newlineDelimiter, "newlineDelimiter cannot be null"); + return this; + } + + public Builder crlfNewlineDelimiter() { + return newlineDelimiter(CRLF); + } + + public Builder lfNewlineDelimiter() { + return newlineDelimiter(LF); + } + + public Builder fieldNamesComparator(Comparator fieldNamesComparator) { + this.fieldNamesComparator = requireNonNull(fieldNamesComparator, "fieldNamesComparator cannot be null"); + return this; + } + + public AsteriskObjectMapper build() { + return new AsteriskObjectMapper(newlineDelimiter, fieldNamesComparator); + } + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/annotation/AsteriskName.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/annotation/AsteriskName.java new file mode 100644 index 00000000..dbced9af --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/annotation/AsteriskName.java @@ -0,0 +1,37 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Marker annotation that can be used to define a logical property name. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +@Target({METHOD}) +@Retention(RUNTIME) +public @interface AsteriskName { + /** + * Defines the name of the logical property, i.e., the AMI action field name. + */ + String value() default ""; +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/annotation/AsteriskSerialize.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/annotation/AsteriskSerialize.java new file mode 100644 index 00000000..c63d7a6a --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/annotation/AsteriskSerialize.java @@ -0,0 +1,39 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.annotation; + +import org.asteriskjava.core.databind.serializer.AsteriskSerializer; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation used for configuring serialization aspects, by attaching to "getter" methods. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +@Target({METHOD}) +@Retention(RUNTIME) +public @interface AsteriskSerialize { + /** + * Serializer class to use for serializing associated value. + */ + Class> value(); +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/AsteriskSerializer.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/AsteriskSerializer.java new file mode 100644 index 00000000..f885a27e --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/AsteriskSerializer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer; + +import org.asteriskjava.core.databind.AsteriskGenerator; + +/** + * Interface representing a serializer for a given type. + * + * @param type of the serialized value + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public interface AsteriskSerializer { + /** + * Serializes object into Asterisk string. + * + * @param fieldName field name of the currently serialized object + * @param value object to serialize + * @param asteriskGenerator generator used to write a Java object to an Asterisk string + */ + void serialize(String fieldName, T value, AsteriskGenerator asteriskGenerator); +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/WritableFileName.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/WritableFileName.java new file mode 100644 index 00000000..e97aa339 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/WritableFileName.java @@ -0,0 +1,27 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer; + +import org.asteriskjava.core.databind.AsteriskGenerator; + +/** + * Marker interface for writing the field name in the serializer implementation instead of {@link AsteriskGenerator}. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public interface WritableFileName { +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/custom/ComaJoiningSerializer.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/custom/ComaJoiningSerializer.java new file mode 100644 index 00000000..5871206e --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/custom/ComaJoiningSerializer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer.custom; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.asteriskjava.core.databind.serializer.AsteriskSerializer; + +import java.util.Collection; + +import static java.util.stream.Collectors.joining; + +/** + * Serializer for joining collection elements by calling their toString method, using a comma as the 'glue'. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class ComaJoiningSerializer implements AsteriskSerializer> { + private static final String COMA_SEPARATOR = ","; + + @Override + public void serialize(String fieldName, Collection value, AsteriskGenerator asteriskGenerator) { + String fieldValue = value + .stream() + .map(Object::toString) + .collect(joining(COMA_SEPARATOR)); + + asteriskGenerator.writeFieldValue(fieldValue); + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/custom/VariableSerializer.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/custom/VariableSerializer.java new file mode 100644 index 00000000..9acb02f8 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/custom/VariableSerializer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer.custom; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.asteriskjava.core.databind.serializer.AsteriskSerializer; +import org.asteriskjava.core.databind.serializer.WritableFileName; + +import java.util.Map; + +import static java.lang.String.format; + +/** + * Serializer for key=value pairs with an additional field name writer. + *

+ * Following code: + *

+ * @AsteriskSerialize(VariableSerializer.class)
+ * public Map<String, String> getVariable() {
+ *     ...
+ * }
+ * 
+ * would produce: + *
+ * Variable: key1=value1
+ * Variable: key2=value2
+ * Variable: key3=value3
+ * 
+ * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class VariableSerializer implements AsteriskSerializer>, WritableFileName { + @Override + public void serialize(String fieldName, Map value, AsteriskGenerator asteriskGenerator) { + value.forEach((key, v) -> { + asteriskGenerator.writeFieldName(fieldName); + asteriskGenerator.writeFieldValue(format("%s=%s", key, v)); + }); + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/std/ToStringSerializer.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/std/ToStringSerializer.java new file mode 100644 index 00000000..46d4b6b4 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/serializer/std/ToStringSerializer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer.std; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.asteriskjava.core.databind.serializer.AsteriskSerializer; + +/** + * Base serializer which calls only the toString method on the passed value. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class ToStringSerializer implements AsteriskSerializer { + @Override + public void serialize(String fieldName, Object value, AsteriskGenerator asteriskGenerator) { + asteriskGenerator.writeFieldValue(value.toString()); + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/utils/ReflectionUtils.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/utils/ReflectionUtils.java new file mode 100644 index 00000000..19fe0ce8 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/utils/ReflectionUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.utils; + +import java.lang.reflect.Method; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +import static java.lang.reflect.Modifier.*; + +/** + * Convenient class to deal with getters from mapped classes. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public final class ReflectionUtils { + /** + * Returns a {@link Map} of getter methods of the given class. + *

+ * The key of the map contains the name of the attribute that can be accessed by the getter, the value being the + * getter itself (an instance of Method). A method is considered a getter if its name starts with 'get' or 'is'. + * It is declared public and takes no arguments. + * + * @param clazz the class to return the getters for + * @param comparator the comparator for sorting properties + * @return a Map of attributes and their accessor methods (getters) + * @see #getGetters(Class) + */ + public static Map getGetters(Class clazz, Comparator comparator) { + Map accessors = comparator != null ? new TreeMap<>(comparator) : new LinkedHashMap<>(); + + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getParameterCount() > 0 || + method.getReturnType() == Void.TYPE || + !isPublic(method.getModifiers()) || + isNative(method.getModifiers()) || + isAbstract(method.getModifiers()) || + isStatic(method.getModifiers()) || + method.getName().equals("toString") + ) { + continue; + } + + String name = null; + String methodName = method.getName(); + + if (methodName.startsWith("get")) { + name = methodName.substring(3); + } else if (methodName.startsWith("is")) { + name = methodName.substring(2); + } + + if (name == null || name.isEmpty()) { + continue; + } + + accessors.put(name, method); + } + + return accessors; + } + + /** + * Returns a Map of getter methods of the given class. + * + * @param clazz the class to return the getters for + * @return a Map of attributes and their accessor methods (getters) + * @see #getGetters(Class, Comparator) + */ + public static Map getGetters(Class clazz) { + return getGetters(clazz, null); + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectMethodWriter.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectMethodWriter.java new file mode 100644 index 00000000..eb33c1dd --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectMethodWriter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.writer; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.asteriskjava.core.databind.serializer.AsteriskSerializer; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Writer class to write field names and serialized values using the {@link AsteriskGenerator}. + * + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class AsteriskObjectMethodWriter { + private final Method method; + private final String name; + private final AsteriskSerializer asteriskSerializer; + private final AsteriskObjectMethodWriterContext asteriskObjectMethodWriterContext; + + public AsteriskObjectMethodWriter( + Method method, + String name, + AsteriskSerializer asteriskSerializer, + AsteriskObjectMethodWriterContext asteriskObjectMethodWriterContext + ) { + this.method = method; + this.name = name; + this.asteriskSerializer = asteriskSerializer; + this.asteriskObjectMethodWriterContext = asteriskObjectMethodWriterContext; + } + + public void writeName(AsteriskGenerator asteriskGenerator) { + if (!asteriskObjectMethodWriterContext.serializerWriteFieldName()) { + asteriskGenerator.writeFieldName(name); + } + } + + public void writeValue(Object obj, AsteriskGenerator asteriskGenerator) { + Object currentValue = getCurrentValue(obj); + asteriskSerializer.serialize(name, currentValue, asteriskGenerator); + } + + private Object getCurrentValue(Object obj) { + try { + return method.invoke(obj); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectMethodWriterContext.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectMethodWriterContext.java new file mode 100644 index 00000000..26b03ce5 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectMethodWriterContext.java @@ -0,0 +1,26 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.writer; + +/** + * Context for the process methods. + * + * @param serializerWriteFieldName whatever serializer write field names + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public record AsteriskObjectMethodWriterContext(boolean serializerWriteFieldName) { +} diff --git a/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectWriter.java b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectWriter.java new file mode 100644 index 00000000..91d6ad71 --- /dev/null +++ b/asterisk-java-core/src/main/java/org/asteriskjava/core/databind/writer/AsteriskObjectWriter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.writer; + +import org.asteriskjava.core.databind.annotation.AsteriskName; +import org.asteriskjava.core.databind.annotation.AsteriskSerialize; +import org.asteriskjava.core.databind.serializer.AsteriskSerializer; +import org.asteriskjava.core.databind.serializer.WritableFileName; +import org.asteriskjava.core.databind.serializer.std.ToStringSerializer; + +import java.lang.reflect.Method; +import java.util.Comparator; +import java.util.List; +import java.util.Map.Entry; + +import static org.asteriskjava.core.databind.utils.ReflectionUtils.getGetters; + +/** + * @author Piotr Olaszewski + * @since 4.0.0 + */ +public class AsteriskObjectWriter { + private final Class clazz; + private final Comparator fieldNamesComparator; + + public AsteriskObjectWriter(Class clazz, Comparator fieldNamesComparator) { + this.clazz = clazz; + this.fieldNamesComparator = fieldNamesComparator; + } + + public List getAsteriskObjectMethodWriters() { + return getGetters(clazz, fieldNamesComparator) + .entrySet() + .stream() + .map(this::getAsteriskObjectMethodWriter) + .toList(); + } + + private AsteriskObjectMethodWriter getAsteriskObjectMethodWriter(Entry entry) { + Method method = entry.getValue(); + + String name = getName(method, entry.getKey()); + + AsteriskSerializer asteriskSerializer = getAsteriskSerializer(method); + + boolean serializerWriteFieldName = asteriskSerializer instanceof WritableFileName; + AsteriskObjectMethodWriterContext context = new AsteriskObjectMethodWriterContext(serializerWriteFieldName); + + return new AsteriskObjectMethodWriter(method, name, asteriskSerializer, context); + } + + private static String getName(Method method, String name) { + AsteriskName asteriskName = method.getAnnotation(AsteriskName.class); + return asteriskName == null ? name : asteriskName.value(); + } + + private AsteriskSerializer getAsteriskSerializer(Method method) { + AsteriskSerialize asteriskSerialize = method.getAnnotation(AsteriskSerialize.class); + if (asteriskSerialize == null) { + return new ToStringSerializer(); + } + + Class> asteriskSerializerClass = asteriskSerialize.value(); + try { + //noinspection unchecked + return (AsteriskSerializer) asteriskSerializerClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Cannot create new instance of serializer %s".formatted(asteriskSerialize), e); + } + } +} diff --git a/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/AsteriskObjectMapperTest.java b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/AsteriskObjectMapperTest.java new file mode 100644 index 00000000..de7a0f54 --- /dev/null +++ b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/AsteriskObjectMapperTest.java @@ -0,0 +1,80 @@ +package org.asteriskjava.core.databind; + +import org.asteriskjava.core.databind.annotation.AsteriskName; +import org.asteriskjava.core.databind.annotation.AsteriskSerialize; +import org.asteriskjava.core.databind.serializer.custom.ComaJoiningSerializer; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.asteriskjava.core.NewlineDelimiter.CRLF; +import static org.asteriskjava.core.databind.AsteriskObjectMapper.builder; +import static org.asteriskjava.core.databind.AsteriskObjectMapperTest.SimpleBean.AuthType.MD5; + +class AsteriskObjectMapperTest { + private final AsteriskObjectMapper asteriskObjectMapper = builder() + .crlfNewlineDelimiter() + .build(); + + @Test + void shouldGenerateSimpleBean() { + //given + SimpleBean bean = new SimpleBean(); + bean.setActionId("id-1"); + bean.setAuthType(MD5); + bean.setCodecs(List.of("codec1", "codec2")); + + //when + String string = asteriskObjectMapper.writeValue(bean); + + //then + String expected = "Action: SimpleBean" + CRLF.getPattern(); + expected += "ActionID: id-1" + CRLF.getPattern(); + expected += "AuthType: MD5" + CRLF.getPattern(); + expected += "Codecs: codec1,codec2" + CRLF.getPattern(); + assertThat(string).isEqualTo(expected); + } + + public static class SimpleBean { + public enum AuthType { + MD5, + } + + private String actionId; + + private AuthType authType; + + private List codecs; + + public String getAction() { + return "SimpleBean"; + } + + @AsteriskName("ActionID") + public String getActionId() { + return actionId; + } + + public void setActionId(String actionId) { + this.actionId = actionId; + } + + public AuthType getAuthType() { + return authType; + } + + public void setAuthType(AuthType authType) { + this.authType = authType; + } + + @AsteriskSerialize(ComaJoiningSerializer.class) + public List getCodecs() { + return codecs; + } + + public void setCodecs(List codecs) { + this.codecs = codecs; + } + } +} diff --git a/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/custom/ComaJoiningSerializerTest.java b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/custom/ComaJoiningSerializerTest.java new file mode 100644 index 00000000..b5411890 --- /dev/null +++ b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/custom/ComaJoiningSerializerTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer.custom; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.junit.jupiter.api.Test; + +import java.util.EnumSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.asteriskjava.core.NewlineDelimiter.CRLF; + +class ComaJoiningSerializerTest { + private final AsteriskGenerator asteriskGenerator = new AsteriskGenerator(CRLF); + + @Test + void shouldSerializeList() { + //given + ComaJoiningSerializer comaJoiningSerializer = new ComaJoiningSerializer(); + + List value = List.of("string1", "string2", "string3"); + + //when + comaJoiningSerializer.serialize("fieldName", value, asteriskGenerator); + + //then + assertThat(asteriskGenerator.generate().trim()).isEqualTo("string1,string2,string3"); + } + + @Test + void shouldSerializeListOfEnums() { + //given + ComaJoiningSerializer comaJoiningSerializer = new ComaJoiningSerializer(); + + EnumSet enums = EnumSet.of(SampleEnum.value1, SampleEnum.value2, SampleEnum.value3); + + //when + comaJoiningSerializer.serialize("fieldName", enums, asteriskGenerator); + + //then + assertThat(asteriskGenerator.generate().trim()).isEqualTo("value1,value2,value3"); + } + + private enum SampleEnum { + value1, + value2, + value3, + } +} diff --git a/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/custom/VariableSerializerTest.java b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/custom/VariableSerializerTest.java new file mode 100644 index 00000000..02c69f9d --- /dev/null +++ b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/custom/VariableSerializerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer.custom; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.asteriskjava.core.NewlineDelimiter.CRLF; + +class VariableSerializerTest { + private final AsteriskGenerator asteriskGenerator = new AsteriskGenerator(CRLF); + + @Test + void shouldSerializeValues() { + //given + VariableSerializer variableSerializer = new VariableSerializer(); + + Map map = Map.of( + "key1", "value1", + "key2", "value2", + "key3", "value3" + ); + + //when + variableSerializer.serialize("fieldName", map, asteriskGenerator); + + //then + assertThat(asteriskGenerator.generate()) + .contains( + "fieldName: key1=value1", + "fieldName: key2=value2", + "fieldName: key3=value3" + ); + } +} diff --git a/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/std/ToStringSerializerTest.java b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/std/ToStringSerializerTest.java new file mode 100644 index 00000000..0378e957 --- /dev/null +++ b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/serializer/std/ToStringSerializerTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.serializer.std; + +import org.asteriskjava.core.databind.AsteriskGenerator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.asteriskjava.core.NewlineDelimiter.CRLF; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class ToStringSerializerTest { + private final AsteriskGenerator asteriskGenerator = new AsteriskGenerator(CRLF); + + @ParameterizedTest + @MethodSource("toStringSerializerArguments") + void shouldSerializeUsingToString(Object actual, String expected) { + //given + ToStringSerializer toStringSerializer = new ToStringSerializer(); + + //when + toStringSerializer.serialize("fieldName", actual, asteriskGenerator); + + //then + assertThat(asteriskGenerator.generate().trim()).isEqualTo(expected); + } + + private static Stream toStringSerializerArguments() { + return Stream.of( + arguments("string", "string"), + arguments(true, "true"), + arguments(1.12, "1.12") + ); + } +} diff --git a/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/utils/ReflectionUtilsTest.java b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/utils/ReflectionUtilsTest.java new file mode 100644 index 00000000..29f9dccb --- /dev/null +++ b/asterisk-java-core/src/test/java/org/asteriskjava/core/databind/utils/ReflectionUtilsTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2004-2023 Asterisk Java contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asteriskjava.core.databind.utils; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Comparator; +import java.util.Map; +import java.util.StringJoiner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.asteriskjava.core.databind.utils.ReflectionUtils.getGetters; + +class ReflectionUtilsTest { + @Test + void shouldDoesNotReturnGetterWhenHasAnyParameters() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("WithParameters"); + } + + @Test + void shouldDoesNotReturnGetterWhenHasVoidType() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("WithVoidType"); + } + + @Test + void shouldDoesNotReturnGetterWhenHasNotPublicScope() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("WhichIsHasNotPublicScope"); + } + + @Test + void shouldDoesNotReturnGetterWhenIsNative() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("WithNative"); + } + + @Test + void shouldDoesNotReturnGetterWhenIsAbstract() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("WithAbstract"); + } + + @Test + void shouldDoesNotReturnGetterWhenIsStatic() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("WithStatic"); + } + + @Test + void shouldDoesNotReturnGetterWhenIsToString() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).doesNotContainKey("toString"); + } + + @Test + void shouldDoesNotReturnGetterWhenNameHasOnlyGet() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).isEmpty(); + } + + @Test + void shouldDoesNotReturnGetterWhenNameHasOnlyIs() { + //when + Map getters = getGetters(InvalidClass.class); + + //then + assertThat(getters).isEmpty(); + } + + static abstract class InvalidClass { + public String getWithParameters(String parameter) { + return parameter; + } + + public void getWithVoidType() { + } + + String getWhichIsHasNotPublicScope() { + return ""; + } + + public native String getWithNative(); + + public abstract String getWithAbstract(); + + public static String getWithStatic() { + return ""; + } + + public String get() { + return ""; + } + + public String is() { + return ""; + } + + @Override + public String toString() { + return new StringJoiner(", ", InvalidClass.class.getSimpleName() + "[", "]") + .toString(); + } + } + + @Test + void shouldReturnValidGetters() { + //when + Map getters = getGetters(ValidClass.class); + + //then + assertThat(getters).containsOnlyKeys("Value", "Valid", "Action"); + } + + @Test + void shouldReturnValidGettersSorted() { + //when + Map getters = getGetters(ValidClass.class, new ActionFieldsComparator()); + + //then + assertThat(getters.keySet()).containsExactly("Action", "Value", "Valid"); + } + + static class ValidClass { + + public String getValue() { + return "value"; + } + + public boolean isValid() { + return true; + } + + public String getAction() { + return "NewAction"; + } + } + + static class ActionFieldsComparator implements Comparator { + @Override + public int compare(String o1, String o2) { + if (o1.equals(o2)) { + return 0; + } + return o1.equalsIgnoreCase("Action") ? -1 : 1; + } + } +} diff --git a/build.gradle b/build.gradle index 73d0c168..f7feed54 100644 --- a/build.gradle +++ b/build.gradle @@ -27,14 +27,14 @@ repositories { dependencies { implementation 'com.google.guava:guava:32.1.3-jre' - implementation 'org.apache.logging.log4j:log4j-core:2.21.1' + implementation 'org.apache.logging.log4j:log4j-core:2.22.0' implementation 'org.reflections:reflections:0.10.2' implementation 'org.slf4j:slf4j-api:2.0.9' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' - testImplementation 'org.mockito:mockito-core:4.11.0' - testImplementation 'ch.qos.logback:logback-classic:1.3.11' + testImplementation 'org.mockito:mockito-core:5.7.0' + testImplementation 'ch.qos.logback:logback-classic:1.4.11' } tasks.named('test') { diff --git a/settings.gradle b/settings.gradle index 553c1b13..ccfa47cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ rootProject.name = 'asterisk-java' + +include 'asterisk-java-core'