diff --git a/core/src/main/java/io/jstach/rainbowgum/ConfigObject.java b/core/src/main/java/io/jstach/rainbowgum/ConfigObject.java new file mode 100644 index 00000000..25440325 --- /dev/null +++ b/core/src/main/java/io/jstach/rainbowgum/ConfigObject.java @@ -0,0 +1,30 @@ +package io.jstach.rainbowgum; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Used to generate Rainbow Gum config objects + */ +@Retention(CLASS) +@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD }) +@Documented +public @interface ConfigObject { + + /** + * Name of builder. + * @return name of builder. + */ + String name(); + + /** + * Property prefix. + * @return prefix + */ + String prefix(); + +} diff --git a/core/src/main/java/io/jstach/rainbowgum/LogConfig.java b/core/src/main/java/io/jstach/rainbowgum/LogConfig.java index bd81202b..1122d74c 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogConfig.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogConfig.java @@ -148,7 +148,7 @@ public void subscribe(Consumer consumer) { @Override public boolean isEnabled(String loggerName) { - return changeSetting.require(_config().properties(), loggerName); + return changeSetting.value(_config().properties(), loggerName); } diff --git a/core/src/main/java/io/jstach/rainbowgum/LogOutput.java b/core/src/main/java/io/jstach/rainbowgum/LogOutput.java index 181693b5..e748d78f 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogOutput.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogOutput.java @@ -70,6 +70,45 @@ private static URI uri(String s) { } } + /** + * The content type of the binary data passed to the output from an encoder. + */ + public interface ContentType { + + /** + * Content type of binary datay passed to output. + * @return content type. + */ + String contentType(); + + /** + * Builtin content types. + */ + public enum StandardContentType implements ContentType { + + /** + * text/plain + */ + TEXT_PLAIN() { + @Override + public String contentType() { + return "text/plain"; + } + }, + /** + * application/json + */ + APPLICATION_JSON() { + @Override + public String contentType() { + return "application/json"; + } + } + + } + + } + /** * The uri of the output. * @return uri. @@ -89,16 +128,17 @@ default void start(LogConfig config) { * @param s string. */ default void write(LogEvent event, String s) { - write(event, s.getBytes(StandardCharsets.UTF_8)); + write(event, s.getBytes(StandardCharsets.UTF_8), ContentType.StandardContentType.TEXT_PLAIN); } /** * Analogous to {@link OutputStream#write(byte[])}. * @param event event associated with bytes. * @param bytes data to be written. + * @param contentType content type of the bytes. */ - default void write(LogEvent event, byte[] bytes) { - write(event, bytes, 0, bytes.length); + default void write(LogEvent event, byte[] bytes, ContentType contentType) { + write(event, bytes, 0, bytes.length, contentType); } /** @@ -107,19 +147,21 @@ default void write(LogEvent event, byte[] bytes) { * @param bytes data to be written. * @param off offset. * @param len length. + * @param contentType content type of the bytes. */ - public void write(LogEvent event, byte[] bytes, int off, int len); + public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType); /** * Write event with byte buffer. * @param event event. * @param buf byte buffer. + * @param contentType content type of the buf */ - default void write(LogEvent event, ByteBuffer buf) { + default void write(LogEvent event, ByteBuffer buf, ContentType contentType) { byte[] arr = new byte[buf.position()]; buf.rewind(); buf.get(arr); - write(event, arr); + write(event, arr, contentType); } public void flush(); @@ -154,12 +196,12 @@ public enum WriteMethod { */ STRING, /** - * Prefer calling {@link LogOutput#write(LogEvent, byte[])} or - * {@link LogOutput#write(LogEvent, byte[], int, int)}. + * Prefer calling {@link LogOutput#write(LogEvent, byte[], ContentType)} or + * {@link LogOutput#write(LogEvent, byte[], int, int, ContentType)}. */ BYTES, /** - * Prefer calling {@link LogOutput#write(LogEvent, ByteBuffer)}. + * Prefer calling {@link LogOutput#write(LogEvent, ByteBuffer, ContentType)}. */ BYTE_BUFFER @@ -211,41 +253,6 @@ public static LogOutput ofStandardErr() { return new StdErrOutput(fos.getChannel(), fos); } - // public static LogOutput of(OutputStream out) { - // return new LogOutput() { - // - // @Override - // public void write(LogEvent event, byte[] bytes, int off, int len) { - // try { - // out.write(bytes, off, len); - // } - // catch (IOException e) { - // throw new UncheckedIOException(e); - // } - // } - // - // @Override - // public void flush() { - // try { - // out.flush(); - // } - // catch (IOException e) { - // throw new UncheckedIOException(e); - // } - // } - // - // @Override - // public void close() { - // try { - // out.close(); - // } - // catch (IOException e) { - // throw new UncheckedIOException(e); - // } - // } - // }; - // } - /** * Creates an output from a file channel. * @param uri uri of output. @@ -281,8 +288,8 @@ public URI uri() throws UnsupportedOperationException { } @Override - public synchronized void write(LogEvent event, byte[] bytes, int off, int len) { - output.write(event, bytes, off, len); + public synchronized void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) { + output.write(event, bytes, off, len, contentType); } @Override @@ -320,12 +327,12 @@ public URI uri() throws UnsupportedOperationException { } @Override - public void write(LogEvent event, byte[] bytes, int off, int len) { - write(event, ByteBuffer.wrap(bytes, off, len)); + public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) { + write(event, ByteBuffer.wrap(bytes, off, len), contentType); } @Override - public void write(LogEvent event, ByteBuffer buffer) { + public void write(LogEvent event, ByteBuffer buffer, ContentType contentType) { try { // Clear any current interrupt (see LOGBACK-875) @@ -396,7 +403,7 @@ public Std(URI uri, FileChannel channel, OutputStream outputStream) { } @Override - public void write(LogEvent event, ByteBuffer buffer) { + public void write(LogEvent event, ByteBuffer buffer, ContentType contentType) { try { channel.write(buffer); } @@ -455,7 +462,7 @@ public URI uri() { } @Override - public void write(LogEvent event, byte[] bytes, int off, int len) { + public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) { try { outputStream.write(bytes, off, len); } diff --git a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java index 1c1a2d70..e4bae8a3 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java @@ -380,6 +380,15 @@ public PropertyValue map(PropertyFunctionnull. + * @return value. + */ + public @Nullable T valueOrNull(@Nullable T fallback) { + return property.propertyGetter.valueOrNull(properties, property.key, fallback); + } + /** * Convenience that turns a value into an optional. * @return optional. @@ -395,7 +404,19 @@ public Optional optional() { * @throws NoSuchElementException if there is no value. */ public T value() throws NoSuchElementException { - return property.propertyGetter.require(properties, property.key); + return property.propertyGetter.value(properties, property.key); + } + + /** + * Gets a value if there if not uses the fallback if not null otherwise throws an + * exception. + * @param fallback maybe null. + * @return value. + * @throws NoSuchElementException if no property and fallback is + * null. + */ + public T value(@Nullable T fallback) throws NoSuchElementException { + return property.propertyGetter.value(properties, property.key, fallback); } } @@ -423,7 +444,7 @@ public PropertyValue get(LogProperties properties) { * @param mapper function to map. * @return property. */ - private Property map(PropertyFunction mapper) { + public Property map(PropertyFunction mapper) { return new Property<>(propertyGetter.map(mapper), key); } @@ -452,6 +473,21 @@ sealed interface PropertyGetter { @Nullable T valueOrNull(LogProperties props, String key); + /** + * Value or fallback if property is missing. + * @param props log properties. + * @param key key to use. + * @param fallback to use can be null. + * @return value or null. + */ + default @Nullable T valueOrNull(LogProperties props, String key, @Nullable T fallback) { + var t = valueOrNull(props, key); + if (t == null) { + return fallback; + } + return t; + } + /** * Determines full name of key. * @param key key. @@ -468,7 +504,7 @@ default String fullyQualifiedKey(String key) { * @return value. * @throws NoSuchElementException if no value is found for key. */ - default T require(LogProperties props, String key) throws NoSuchElementException { + default T value(LogProperties props, String key) throws NoSuchElementException { var t = valueOrNull(props, key); if (t == null) { throw findRoot(this).throwMissing(props, key); @@ -476,6 +512,26 @@ default T require(LogProperties props, String key) throws NoSuchElementException return t; } + /** + * Value or fallback or exception if property is missing and fallback is null. + * @param props log properties. + * @param key key to use. + * @param fallback to use can be null. + * @return value or null. + * @throws NoSuchElementException if property is missing and fallback is + * null. + */ + default T value(LogProperties props, String key, @Nullable T fallback) throws NoSuchElementException { + var t = valueOrNull(props, key); + if (t == null) { + t = fallback; + } + if (t == null) { + throw findRoot(this).throwMissing(props, key); + } + return t; + } + /** * Creates a Property from the given key and this getter. * @param key key. @@ -573,6 +629,18 @@ public String fullyQualifiedKey(String key) { return concatKey(prefix, key); } + public PropertyGetter toInt() { + return this.map(Integer::parseInt); + } + + public > PropertyGetter toEnum(Class enumClass) { + return this.map(s -> Enum.valueOf(enumClass, s)); + } + + public PropertyGetter toURI() { + return this.map(URI::new); + } + } /** diff --git a/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java b/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java index e3ad840e..0956afae 100644 --- a/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java +++ b/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java @@ -7,6 +7,7 @@ import io.jstach.rainbowgum.LogEvent; import io.jstach.rainbowgum.LogOutput; +import io.jstach.rainbowgum.LogOutput.ContentType.StandardContentType; class RawJsonWriter { @@ -396,7 +397,7 @@ public final void toStream(final OutputStream stream) throws IOException { } public final void write(final LogOutput output, LogEvent event) { - output.write(event, buffer, 0, position); + output.write(event, buffer, 0, position, StandardContentType.APPLICATION_JSON); position = 0; } diff --git a/core/src/main/java/io/jstach/rainbowgum/output/ListLogOutput.java b/core/src/main/java/io/jstach/rainbowgum/output/ListLogOutput.java index 9ac78991..eccc3f74 100644 --- a/core/src/main/java/io/jstach/rainbowgum/output/ListLogOutput.java +++ b/core/src/main/java/io/jstach/rainbowgum/output/ListLogOutput.java @@ -41,7 +41,7 @@ public void write(LogEvent event, String s) { } @Override - public void write(LogEvent event, byte[] bytes, int off, int len) { + public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) { write(event, new String(bytes, off, len, StandardCharsets.UTF_8)); } diff --git a/pom.xml b/pom.xml index 9afa3962..47dd95b9 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,11 @@ ${project.groupId} rainbowgum-avaje-config ${project.version} + + + ${project.groupId} + rainbowgum-config-apt + ${project.version} @@ -166,6 +171,20 @@ true provided + + io.jstach.pistachio + pistachio-prism + ${pistachio.version} + true + provided + + + io.jstach.pistachio + pistachio-prism-apt + ${pistachio.version} + true + provided + io.jstach jstachio-annotation @@ -190,6 +209,11 @@ disruptor 4.0.0.RC1 + + com.rabbitmq + amqp-client + 5.20.0 + @@ -217,7 +241,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.12.0 + 3.12.1 ${java.version} ${java.version} @@ -754,5 +778,7 @@ rainbowgum-avaje-config test etc + rainbowgum-rabbitmq + rainbowgum-config-apt diff --git a/rainbowgum-config-apt/pom.xml b/rainbowgum-config-apt/pom.xml new file mode 100644 index 00000000..b8284916 --- /dev/null +++ b/rainbowgum-config-apt/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + io.jstach.rainbowgum + rainbowgum-maven-parent + 0.1.3-SNAPSHOT + + rainbowgum-config-apt + + + io.jstach.rainbowgum + rainbowgum-core + true + provided + + + io.jstach + jstachio-annotation + true + provided + + + io.jstach + jstachio-apt + true + provided + + + io.jstach.pistachio + pistachio-prism + + + io.jstach.pistachio + pistachio-prism-apt + + + io.jstach.pistachio + pistachio-svc + + + io.jstach.pistachio + pistachio-svc-apt + + + \ No newline at end of file diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java new file mode 100644 index 00000000..0e96b07a --- /dev/null +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java @@ -0,0 +1,54 @@ +package io.jstach.rainbowgum.apt; + +import java.util.List; + +import io.jstach.jstache.JStache; +import io.jstach.jstache.JStacheConfig; +import io.jstach.jstache.JStacheType; + +@JStacheConfig(type = JStacheType.STACHE) +@JStache(path = "io/jstach/rainbowgum/apt/ConfigBuilder.java") +public record BuilderModel( // + String builderName, // + String propertyPrefix, // + String packageName, // + String targetType, // + String factoryMethod, // + List properties) { + + public String nullableAnnotation() { + return "org.eclipse.jdt.annotation.Nullable"; + } + + public record PropertyModel(String name, String type, String typeWithAnnotation, String defaultValue, + boolean required) { + + private static final String INTEGER_TYPE = "java.lang.Integer"; + + private static final String URI_TYPE = "java.net.URI"; + + private static final String STRING_TYPE = "java.lang.String"; + + public String propertyVar() { + return "property_" + name; + } + + public String propertyLiteral() { + return "PROPERTY_" + name; + } + + public String convertMethod() { + return switch (type) { + case INTEGER_TYPE -> ".toInt()"; + case STRING_TYPE -> ""; + case URI_TYPE -> ".toURI()"; + default -> throw new IllegalStateException(type + " is not supported"); + }; + } + + public String valueMethod() { + return required ? "value" : "valueOrNull"; + } + } + +} diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java new file mode 100644 index 00000000..28c46de8 --- /dev/null +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java @@ -0,0 +1,360 @@ +package io.jstach.rainbowgum.apt; + +import static java.util.Objects.requireNonNull; + +import java.beans.Introspector; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementScanner8; +import javax.lang.model.util.Elements; +import javax.lang.model.util.SimpleTypeVisitor8; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import io.jstach.rainbowgum.apt.prism.ConfigObjectPrism; +import io.jstach.svc.ServiceProvider; + +@SupportedAnnotationTypes({ ConfigObjectPrism.PRISM_ANNOTATION_TYPE }) +@ServiceProvider(value = Processor.class) +public class ConfigProcessor extends AbstractProcessor { + + // Set configBeanDefinitions = + // Collections.newSetFromMap(new ConcurrentHashMap<>()); + + private static final String CONFIG_BEAN_CLASS = ConfigObjectPrism.PRISM_ANNOTATION_TYPE; + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + final Helper h = new Helper(requireNonNull(processingEnv)); + if (!roundEnv.processingOver()) { + TypeElement configBeanElement = processingEnv.getElementUtils().getTypeElement(CONFIG_BEAN_CLASS); + if (configBeanElement == null) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Config library not in classpath"); + throw new NullPointerException("ConfigBuilder element missing"); + } + for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(configBeanElement)) { + if (annotatedElement.getKind() != ElementKind.METHOD) { + processingEnv.getMessager() + .printMessage(Kind.ERROR, "@" + CONFIG_BEAN_CLASS + " should be a method", annotatedElement); + continue; + } + ExecutableElement ee = (ExecutableElement) annotatedElement; + ConfigObjectPrism prism = ConfigObjectPrism.getInstanceOn(annotatedElement); + model(h, prism, ee); + } + } + return false; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Nullable + private BuilderModel model(Helper h, ConfigObjectPrism prism, ExecutableElement ee) { + + TypeElement enclosingType = (TypeElement) ee.getEnclosingElement(); + String builderName = prism.name(); + String propertyPrefix = prism.prefix(); + String packageName = h.getPackageString(enclosingType); + String targetType = h.getFullyQualifiedClassName(ee.getReturnType()); + String factoryMethod = enclosingType + "." + ee.getSimpleName(); + List properties = new ArrayList<>(); + + List parameters = ee.getParameters(); + for (var p : parameters) { + String name = p.getSimpleName().toString(); + String type = h.getFullyQualifiedClassName(p.asType()); + String typeWithAnnotation = ToStringTypeVisitor.toCodeSafeString(p.asType()); + String defaultValue = "null"; + boolean required = !h.isNullable(p.asType()); + var prop = new BuilderModel.PropertyModel(name, type, typeWithAnnotation, defaultValue, required); + properties.add(prop); + } + + var m = new BuilderModel(builderName, propertyPrefix, packageName, targetType, factoryMethod, properties); + String java = BuilderModelRenderer.of().execute(m); + try { + processingEnv.getMessager() + .printMessage(Kind.NOTE, "Generating ConfigBuilder. name: " + m.packageName() + "." + m.builderName()); + JavaFileObject file = h.createSourceFile(ee, m.packageName(), m.builderName()); + try (PrintWriter pw = new PrintWriter(file.openWriter())) { + pw.print(java); + } + } + catch (IOException e1) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to create config builder", ee); + return null; + } + catch (Exception e1) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to create config builder", ee); + return null; + } + return m; + } + + private static Class primitiveToClass(TypeKind k) { + if (!k.isPrimitive()) + throw new IllegalArgumentException("k is not primitive: " + k); + switch (k) { + case BOOLEAN: + return Boolean.class; + case BYTE: + return Byte.class; + case CHAR: + return Character.class; + case DOUBLE: + return Double.class; + case FLOAT: + return Float.class; + case INT: + return Integer.class; + case LONG: + return Long.class; + case SHORT: + return Short.class; + default: + throw new IllegalArgumentException("k is not a primitive" + k); + + } + } + + private static String propertyNameFromMethodName(String propertyName) { + int length = propertyName.length(); + if (length > 3 && propertyName.startsWith("get")) { + return requireNonNull(Introspector.decapitalize(propertyName.substring(3))); + } + else if (length > 2 && propertyName.startsWith("is")) { + return requireNonNull(Introspector.decapitalize(propertyName.substring(2))); + } + return propertyName; + } + + static class Helper { + + private final Types types; + + private final Elements elements; + + private final Filer filer; + + private final Messager messager; + + public Helper(ProcessingEnvironment pe) { + this(pe.getTypeUtils(), pe.getElementUtils(), pe.getFiler(), pe.getMessager()); + } + + public Helper(Types types, Elements elements, Filer filer, Messager messager) { + super(); + this.types = types; + this.elements = elements; + this.filer = filer; + this.messager = messager; + } + + public String getPackageString(TypeElement te) { + return elements.getPackageOf(te).getQualifiedName().toString(); + } + + public JavaFileObject createSourceFile(final Element baseElement, final String packageName, + final String className) throws Exception { + + final String suffix = packageName.isEmpty() ? "" : packageName + "."; + return filer.createSourceFile(suffix + className, baseElement); + } + + public FileObject getResourceFile(final String file) throws IOException { + return filer.getResource(StandardLocation.CLASS_OUTPUT, "", file); + } + + public FileObject createResourceFile(final String file) throws IOException { + return filer.createResource(StandardLocation.CLASS_OUTPUT, "", file); + } + + public Stream getAllMethods(final TypeElement te) { + List ancestors = ancestors(te); + Collections.reverse(ancestors); + ElementScanner8<@Nullable Void, Collection<@NonNull ExecutableElement>> scanner = new ElementScanner8<@Nullable Void, Collection<@NonNull ExecutableElement>>() { + @Override + public @Nullable Void visitExecutable(ExecutableElement e, Collection p) { + p.add(e); + return null; + } + }; + List es = new ArrayList<>(); + scanner.scan(ancestors, es); + return es.stream(); + + } + + public static boolean isPublicVirtual(Set modifiers) { + return modifiers.contains(Modifier.PUBLIC) && !modifiers.contains(Modifier.STATIC) + && !modifiers.contains(Modifier.NATIVE) && !modifiers.contains(Modifier.ABSTRACT); + } + + public static boolean isPublicAbstract(Set modifiers) { + return modifiers.contains(Modifier.PUBLIC) && !modifiers.contains(Modifier.STATIC) + && !modifiers.contains(Modifier.NATIVE) && modifiers.contains(Modifier.ABSTRACT); + } + + public List ancestors(@Nullable final TypeElement e) { + List list = new ArrayList<>(); + @Nullable + TypeElement c = e; + while (c != null) { + list.add(c); + TypeMirror tm = c.getSuperclass(); + TypeElement t = (TypeElement) types.asElement(tm); + if (t == null) + return list; + c = t; + } + return list; + } + + public Predicate reportPredicate(final Kind kind, final String message, + final Predicate p) { + return new Predicate() { + @Override + public boolean test(E e) { + boolean b = p.test(e); + if (!b) + messager.printMessage(kind, message, e); + return b; + } + }; + } + + public boolean isListType(TypeMirror tm) { + DeclaredType dt = (DeclaredType) tm; + return isListType((TypeElement) dt.asElement()); + } + + public boolean isListType(TypeElement te) { + return te.getQualifiedName().toString().equals("java.util.List"); + } + + public String getBinaryName(TypeElement element) { + return getBinaryNameImpl(element, element.getSimpleName().toString()); + } + + public String getBinaryNameImpl(TypeElement element, String className) { + Element enclosingElement = element.getEnclosingElement(); + + if (enclosingElement instanceof PackageElement) { + PackageElement pkg = (PackageElement) enclosingElement; + if (pkg.isUnnamed()) { + return className; + } + return pkg.getQualifiedName() + "." + className; + } + + TypeElement typeElement = (TypeElement) enclosingElement; + return getBinaryNameImpl(typeElement, typeElement.getSimpleName() + "$" + className); + } + + public String getFullyQualifiedClassName(TypeMirror t) { + + if (t.getKind() == TypeKind.DECLARED) { + TypeElement te = requireNonNull((TypeElement) types.asElement(t)); + return te.getQualifiedName().toString(); + } + else { + return t.toString(); + } + + } + + public String getFullyQualifiedClassNameWithTypeAnnotations(TypeMirror t) { + + if (t.getKind() == TypeKind.DECLARED) { + TypeElement te = requireNonNull((TypeElement) types.asElement(t)); + String ats = t.getAnnotationMirrors() + .stream() + .map(am -> typeUseAnnotationFQN(am)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(s -> "@" + s) + .collect(Collectors.joining(" ")); + String packageLikeName; + Element ee = te.getEnclosingElement(); + if (ee instanceof TypeElement) { + packageLikeName = ((TypeElement) ee).getQualifiedName().toString(); + } + else if (ee instanceof PackageElement) { + packageLikeName = ((PackageElement) ee).getQualifiedName().toString(); + } + else { + packageLikeName = ""; + } + packageLikeName = packageLikeName.isEmpty() ? "" : packageLikeName + "."; + ats = ats.isEmpty() ? "" : ats + " "; + return packageLikeName + ats + te.getSimpleName(); + } + else { + return t.toString(); + } + + } + + public boolean isNullable(TypeMirror t) { + if (t.getKind() != TypeKind.DECLARED) { + return false; + } + return t.getAnnotationMirrors() + .stream() + .flatMap(am -> typeUseAnnotationFQN(am).stream()) + .filter(s -> s.endsWith(".Nullable")) + .findAny() + .isPresent(); + } + + Optional typeUseAnnotationFQN( + // TypeElement te, + @Nullable AnnotationMirror am) { + if (am == null) + return Optional.empty(); + DeclaredType dt = am.getAnnotationType(); + return Optional.of(getFullyQualifiedClassName(dt)); + } + + } + +} diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ToStringTypeVisitor.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ToStringTypeVisitor.java new file mode 100644 index 00000000..e3fde8b3 --- /dev/null +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ToStringTypeVisitor.java @@ -0,0 +1,258 @@ +package io.jstach.rainbowgum.apt; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import javax.lang.model.element.Element; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.UnionType; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.AbstractTypeVisitor14; + +public class ToStringTypeVisitor extends AbstractTypeVisitor14 { + + private final int depth; + + private final boolean includeAnnotations; + + private final HashMap typeVariables; + + private static final boolean DEBUG = false; + + public static String toCodeSafeString(TypeMirror typeMirror) { + return toCodeSafeString(typeMirror, 1, Map.of()); + } + + static String toCodeSafeString(TypeMirror typeMirror, int depth, Map typeVariables) { + var v = new ToStringTypeVisitor(depth, typeVariables); + v.typeVariables.putAll(typeVariables); + StringBuilder b = new StringBuilder(); + return typeMirror.accept(v, b).toString(); + } + + private ToStringTypeVisitor() { + this(1, new HashMap<>()); + } + + private ToStringTypeVisitor(int depth, Map typeVariables) { + super(); + this.includeAnnotations = true; + this.depth = depth; + this.typeVariables = new HashMap<>(); + this.typeVariables.putAll(typeVariables); + } + + void debug(String message, Object o) { + if (DEBUG) { + if (depth > 10) { + throw new IllegalStateException(); + } + var out = System.out; + if (out != null) + out.println(indent(depth) + "#" + message + ". " + o); + } + } + + private static String indent(int depth) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append("\t"); + } + return sb.toString(); + } + + ToStringTypeVisitor child() { + ToStringTypeVisitor tmv = new ToStringTypeVisitor(depth + 1, typeVariables); + return tmv; + } + + @Override + public StringBuilder visitPrimitive(PrimitiveType t, StringBuilder p) { + debug("primitive", t); + + if (includeAnnotations) { + for (var ta : t.getAnnotationMirrors()) { + p.append(ta.toString()).append(" "); + } + } + p.append(t.getKind().toString().toLowerCase(Locale.ROOT)); + + return p; + } + + @Override + public StringBuilder visitNull(NullType t, StringBuilder p) { + debug("null", t); + return p; + } + + @Override + public StringBuilder visitArray(ArrayType t, StringBuilder p) { + debug("array", t); + var ct = t.getComponentType(); + ct.accept(child(), p); + boolean first = true; + if (includeAnnotations) { + for (var ta : t.getAnnotationMirrors()) { + if (first) { + p.append(" "); + first = false; + } + p.append(ta.toString()).append(" "); + } + } + p.append("[]"); + return p; + } + + @Override + public StringBuilder visitDeclared(DeclaredType t, StringBuilder p) { + debug("declared", t); + // debug("enclosing type", t.getEnclosingType()); + String fqn = fullyQualfiedName(t, includeAnnotations); + debug("typeUseFQN", fqn); + p.append(fqn); + var tas = t.getTypeArguments(); + if (!tas.isEmpty()) { + p.append("<"); + for (var ta : t.getTypeArguments()) { + ta.accept(child(), p); + } + p.append(">"); + } + return p; + } + + static String fullyQualfiedName(DeclaredType t, boolean includeAnnotations) { + TypeElement element = (TypeElement) t.asElement(); + var typeUseAnnotations = t.getAnnotationMirrors(); + if (typeUseAnnotations.isEmpty() || !includeAnnotations) { + return element.getQualifiedName().toString(); + } + String enclosedPart; + Element enclosed = element.getEnclosingElement(); + if (enclosed instanceof QualifiedNameable qn) { + enclosedPart = qn.getQualifiedName().toString() + "."; + } + else { + enclosedPart = ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(enclosedPart); + for (var ta : typeUseAnnotations) { + sb.append(ta.toString()).append(" "); + } + sb.append(element.getSimpleName()); + return sb.toString(); + } + + @Override + public StringBuilder visitError(ErrorType t, StringBuilder p) { + debug("error", t); + return p; + } + + @Override + public StringBuilder visitTypeVariable(TypeVariable t, StringBuilder p) { + debug("typeVariable", t); + /* + * Types can be recursive so we have to check if we have already done this type. + */ + String previous = typeVariables.get(t); + + if (previous != null) { + p.append(previous); + return p; + } + debug("lower", t.getLowerBound()); + debug("upper", t.getUpperBound()); + StringBuilder sb = new StringBuilder(); + /* + * We do not have to print the upper and lower bound as those are defined usually + * on the method. + */ + if (includeAnnotations) { + for (var ta : t.getAnnotationMirrors()) { + p.append(ta.toString()).append(" "); + sb.append(ta.toString()).append(" "); + } + } + p.append(t.asElement().getSimpleName().toString()); + sb.append(t.asElement().getSimpleName().toString()); + typeVariables.put(t, sb.toString()); + + // debug("upperCorrected", toCodeSafeString(t.getUpperBound(), depth + 1, + // typeVariables)); + + return p; + } + + @Override + public StringBuilder visitWildcard(WildcardType t, StringBuilder p) { + debug("wildcard", t); + var extendsBound = t.getExtendsBound(); + var superBound = t.getSuperBound(); + for (var ta : t.getAnnotationMirrors()) { + p.append(ta.toString()).append(" "); + } + if (extendsBound != null) { + p.append("? extends "); + extendsBound.accept(child(), p); + } + else if (superBound != null) { + p.append("? super "); + superBound.accept(child(), p); + } + else { + p.append("?"); + } + return p; + } + + @Override + public StringBuilder visitExecutable(ExecutableType t, StringBuilder p) { + debug("executable", t); + throw new UnsupportedOperationException(); + } + + @Override + public StringBuilder visitNoType(NoType t, StringBuilder p) { + debug("noType", t); + throw new UnsupportedOperationException(); + } + + @Override + public StringBuilder visitIntersection(IntersectionType t, StringBuilder p) { + debug("intersection", t); + boolean first = true; + for (var b : t.getBounds()) { + if (first) { + first = false; + } + else { + p.append("&"); + } + b.accept(child(), p); + } + return p; + } + + @Override + public StringBuilder visitUnion(UnionType t, StringBuilder p) { + debug("union", t); + throw new UnsupportedOperationException(); + } + +} diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/package-info.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/package-info.java new file mode 100644 index 00000000..fcedfc7e --- /dev/null +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/package-info.java @@ -0,0 +1,2 @@ +@org.eclipse.jdt.annotation.NonNullByDefault +package io.jstach.rainbowgum.apt; \ No newline at end of file diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java new file mode 100644 index 00000000..6f03f455 --- /dev/null +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java @@ -0,0 +1,3 @@ +@io.jstach.prism.GeneratePrisms({ + @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.ConfigObject.class, publicAccess = true) }) +package io.jstach.rainbowgum.apt.prism; \ No newline at end of file diff --git a/rainbowgum-config-apt/src/main/resources/io/jstach/rainbowgum/apt/ConfigBuilder.java b/rainbowgum-config-apt/src/main/resources/io/jstach/rainbowgum/apt/ConfigBuilder.java new file mode 100644 index 00000000..3c6e240b --- /dev/null +++ b/rainbowgum-config-apt/src/main/resources/io/jstach/rainbowgum/apt/ConfigBuilder.java @@ -0,0 +1,44 @@ +{{=$$ $$=}} +package $$packageName$$; + +import io.jstach.rainbowgum.LogProperties; +import io.jstach.rainbowgum.LogProperties.Property; + +public class $$builderName$$ { + + static final String PROPERTY_PREFIX = "$$propertyPrefix$$"; + + $$#properties$$ + static final String $$propertyLiteral$$ = PROPERTY_PREFIX + "$$name$$"; + $$/properties$$ + + $$#properties$$ + static final Property<$$type$$> $$propertyVar$$ = Property.builder()$$convertMethod$$.build($$propertyLiteral$$); + $$/properties$$ + + $$#properties$$ + private $$typeWithAnnotation$$ $$name$$ = $$defaultValue$$; + $$/properties$$ + + $$#properties$$ + public $$builderName$$ $$name$$($$typeWithAnnotation$$ $$name$$) { + this.$$name$$ = $$name$$; + return this; + } + $$/properties$$ + + public $$targetType$$ build() { + return $$factoryMethod$$( + $$#properties$$ + $$^-first$$, $$/-first$$this.$$name$$ + $$/properties$$ + ); + } + + public $$builderName$$ fromProperties(LogProperties properties) { + $$#properties$$ + this.$$name$$ = $$propertyVar$$.get(properties).$$valueMethod$$(this.$$name$$); + $$/properties$$ + return this; + } +} \ No newline at end of file diff --git a/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/ConfigBeanDefinitionModelTest.java b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/ConfigBeanDefinitionModelTest.java new file mode 100644 index 00000000..0ef34681 --- /dev/null +++ b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/ConfigBeanDefinitionModelTest.java @@ -0,0 +1,14 @@ +package io.jstach.rainbowgum.apt; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ConfigBeanDefinitionModelTest { + + @Test + void test() { + fail("Not yet implemented"); + } + +} diff --git a/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java new file mode 100644 index 00000000..ef89cf06 --- /dev/null +++ b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java @@ -0,0 +1,7 @@ +package io.jstach.rainbowgum.apt; + +import io.jstach.rainbowgum.ConfigObject; + +public record Example(Integer count) { + +} diff --git a/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/ExampleBuilder.java b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/ExampleBuilder.java new file mode 100644 index 00000000..3a81066d --- /dev/null +++ b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/ExampleBuilder.java @@ -0,0 +1,30 @@ +package io.jstach.rainbowgum.apt; + +import io.jstach.rainbowgum.LogProperties; +import io.jstach.rainbowgum.LogProperties.Property; + +public class ExampleBuilder { + + private Integer count; + + public static final String PROPERTY_PREFIX = "logging.rabbitmq."; + + public static final String PROPERTY_COUNT = PROPERTY_PREFIX + "count"; + + private static final Property countProperty = Property.builder().toInt().build(PROPERTY_COUNT); + + public ExampleBuilder count(Integer count) { + this.count = count; + return this; + } + + public Example build() { + return new Example(count); + } + + public ExampleBuilder fromProperties(LogProperties properties) { + this.count = countProperty.get(properties).value(this.count); + return this; + } + +} diff --git a/rainbowgum-rabbitmq/pom.xml b/rainbowgum-rabbitmq/pom.xml new file mode 100644 index 00000000..7438edf2 --- /dev/null +++ b/rainbowgum-rabbitmq/pom.xml @@ -0,0 +1,19 @@ + + 4.0.0 + + io.jstach.rainbowgum + rainbowgum-maven-parent + 0.1.3-SNAPSHOT + + rainbowgum-rabbitmq + + + ${project.groupId} + rainbowgum-core + + + com.rabbitmq + amqp-client + + + \ No newline at end of file diff --git a/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQBuilder.java b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQBuilder.java new file mode 100644 index 00000000..febf106e --- /dev/null +++ b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQBuilder.java @@ -0,0 +1,167 @@ +package io.jstach.rainbowgum.rabbitmq; + +import java.net.URI; + +import com.rabbitmq.client.MessageProperties; + +class RabbitMQBuilder { + + /** + * Name of the exchange to publish log events to. + */ + private String exchangeName = "logs"; // done + + /** + * Type of the exchange to publish log events to. + */ + private String exchangeType = "topic"; + + /** + * Configuration arbitrary application ID. + */ + private String applicationId = null; // done + + /** + * A name for the connection (appears on the RabbitMQ Admin UI). + */ + private String connectionName; // done + + /** + * Additional client connection properties added to the rabbit connection, with the + * form {@code key:value[,key:value]...}. + */ + private String clientConnectionProperties; + + /** + * A comma-delimited list of broker addresses: host:port[,host:port]* + * + * @since 1.5.6 + */ + private String addresses; + + /** + * RabbitMQ host to connect to. + */ + private URI uri; + + /** + * RabbitMQ host to connect to. + */ + private String host; + + /** + * RabbitMQ virtual host to connect to. + */ + private String virtualHost; + + /** + * RabbitMQ port to connect to. + */ + private Integer port; + + /** + * RabbitMQ user to connect as. + */ + private String username; + + /** + * RabbitMQ password for this user. + */ + private String password; + + /** + * Use an SSL connection. + */ + private boolean useSsl; + + /** + * The SSL algorithm to use. + */ + private String sslAlgorithm; + + /** + * Location of resource containing keystore and truststore information. + */ + private String sslPropertiesLocation; + + /** + * Keystore location. + */ + private String keyStore; + + /** + * Keystore passphrase. + */ + private String keyStorePassphrase; + + /** + * Keystore type. + */ + private String keyStoreType = "JKS"; + + /** + * Truststore location. + */ + private String trustStore; + + /** + * Truststore passphrase. + */ + private String trustStorePassphrase; + + /** + * Truststore type. + */ + private String trustStoreType = "JKS"; + + /** + * SaslConfig. + * @see RabbitUtils#stringToSaslConfig(String, ConnectionFactory) + */ + private String saslConfig; + + private boolean verifyHostname = true; + + /** + * Default content-type of log messages. + */ + private String contentType = "text/plain"; + + /** + * Default content-encoding of log messages. + */ + private String contentEncoding = null; + + /** + * Whether or not to try and declare the configured exchange when this appender + * starts. + */ + private boolean declareExchange = false; + + /** + * charset to use when converting String to byte[], default null (system default + * charset used). If the charset is unsupported on the current platform, we fall back + * to using the system charset. + */ + private String charset; + + /** + * Whether or not add MDC properties into message headers. true by default for + * backward compatibility + */ + private boolean addMdcAsHeaders = true; + + private boolean durable = true; + + // private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT; + + private boolean autoDelete = false; + + /** + * Used to determine whether {@link MessageProperties#setMessageId(String)} is set. + */ + private boolean generateId = false; + + private boolean includeCallerData; + +} \ No newline at end of file diff --git a/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java new file mode 100644 index 00000000..3cc3129b --- /dev/null +++ b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java @@ -0,0 +1,262 @@ +package io.jstach.rainbowgum.rabbitmq; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.eclipse.jdt.annotation.Nullable; + +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import io.jstach.rainbowgum.LogConfig; +import io.jstach.rainbowgum.LogEvent; +import io.jstach.rainbowgum.LogOutput; +import io.jstach.rainbowgum.LogProperties; +import io.jstach.rainbowgum.LogProperties.Property; + +import io.jstach.rainbowgum.MetaLog; + +public class RabbitMQOutput implements LogOutput { + + private final URI uri; + + private final ConnectionFactory connectionFactory; + + private Connection connection; + + private volatile Channel channel; + + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + private final @Nullable String appId; + + private final String exchange; + + private final String routingKey; + + private final String connectionName; + + private final boolean declareExchange; + + private final String exchangeType; + + private static final String PREFIX = LogProperties.ROOT_PREFIX + "rabbitmq."; + + public static final String EXCHANGE_PROPERTY = PREFIX + "exchange"; + + public static final String ROUTING_KEY_PROPERTY = PREFIX + "routingKey"; + + public static final String CONNECTION_NAME_PROPERTY = PREFIX + "connectionName"; + + public static final String DECLARE_EXCHANGE_PROPERTY = PREFIX + "declareExchange"; + + public static final String EXCHANGE_TYPE_PROPERTY = PREFIX + "exchangeType"; + + public static final String USERNAME_PROPERTY = PREFIX + "username"; + + public static final String PASSWORD_PROPERTY = PREFIX + "password"; + + public static final String PORT_PROPERTY = PREFIX + "port"; + + public static final String HOST_PROPERTY = PREFIX + "host"; + + public static final String VIRTUAL_HOST_PROPERTY = PREFIX + "virtualHost"; + + public static final String APP_ID_PROPERTY = PREFIX + "appId"; + + // public static RabbitMQOutput of(URI uri, LogProperties properties) { + // LogProperties combined = LogProperties.of(List.of(LogProperties.of(uri)), + // properties); + // Property.builder().build(ROUTING_KEY_PROPERTY); + // + // } + + public RabbitMQOutput(URI uri, ConnectionFactory connectionFactory, @Nullable String appId, String exchange, + String routingKey, String connectionName, boolean declareExchange, String exchangeType) { + super(); + this.uri = uri; + this.connectionFactory = connectionFactory; + this.appId = appId; + this.exchange = exchange; + this.routingKey = routingKey; + this.connectionName = connectionName; + this.declareExchange = declareExchange; + this.exchangeType = exchangeType; + } + + public RabbitMQOutput(URI uri, String exchange, String routingKey, @Nullable Boolean declareExchange, + @Nullable String host, @Nullable String username, @Nullable String password, @Nullable Integer port, + @Nullable String appId, @Nullable String connectionName, @Nullable String exchangeType, + @Nullable String virtualHost) throws KeyManagementException, NoSuchAlgorithmException, URISyntaxException { + super(); + this.uri = uri; + this.exchange = exchange; + this.routingKey = routingKey; + this.appId = appId; + this.connectionName = connectionName == null ? "rainbowgumOutput" : connectionName; + this.declareExchange = declareExchange == null ? false : declareExchange; + this.exchangeType = exchangeType == null ? "topic" : exchangeType; + ConnectionFactory factory = new ConnectionFactory(); + factory.setUri(uri); + if (username != null) { + factory.setUsername(username); + } + if (password != null) { + factory.setPassword(password); + } + if (port != null) { + factory.setPort(port); + } + if (host != null) { + factory.setHost(host); + } + if (virtualHost != null) { + factory.setVirtualHost(virtualHost); + } + this.connectionFactory = factory; + } + + @Override + public void start(LogConfig config) { + lock.writeLock().lock(); + try { + this.connection = connectionFactory.newConnection(connectionName); + if (declareExchange) { + var channel = this.connection.createChannel(); + channel.exchangeDeclare(exchange, exchangeType); + } + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + catch (TimeoutException e) { + throw new RuntimeException(e); + } + finally { + lock.writeLock().unlock(); + } + } + + @Override + public URI uri() { + return this.uri; + } + + @Override + public void write(LogEvent event, byte[] bytes, int off, int len, ContentType contentType) { + // https://github.com/rabbitmq/rabbitmq-java-client/issues/422 + byte[] copy = new byte[len]; + System.arraycopy(bytes, off, copy, 0, len); + write(event, bytes, contentType); + } + + @Override + public void write(LogEvent event, byte[] bytes, ContentType contentType) { + BasicProperties props = properties(event, contentType); + byte[] body = bytes; + try { + var c = channel(); + c.basicPublish(exchange, routingKey, props, body); + } + catch (IOException e) { + MetaLog.error(RabbitMQOutput.class, e); + lock.writeLock().lock(); + try { + this.channel = null; + } + finally { + lock.writeLock().unlock(); + } + } + } + + private BasicProperties properties(LogEvent event, ContentType contentType) { + var builder = new BasicProperties.Builder().contentType(contentType.contentType()).appId(appId); + var kvs = event.keyValues(); + if (!kvs.isEmpty()) { + Map headers = new LinkedHashMap<>(kvs.size()); + kvs.forEach(headers::put); + builder.headers(headers); + } + if (appId != null) { + builder.appId(appId); + } + return builder.build(); + } + + Channel channel() throws IOException { + var c = this.channel; + if (c == null) { + lock.writeLock().lock(); + try { + c = this.channel = connection.createChannel(); + if (c == null) { + throw new IOException("channel is unavailable"); + } + return c; + } + finally { + lock.writeLock().unlock(); + } + } + return c; + } + + @Override + public void flush() { + + } + + @Override + public WriteMethod writeMethod() { + return WriteMethod.BYTES; + } + + @Override + public OutputType type() { + return OutputType.NETWORK; + } + + @Override + public void close() { + lock.writeLock().lock(); + try { + var c = this.channel; + var conn = this.connection; + if (c != null) { + try { + c.close(); + } + catch (IOException | TimeoutException e) { + MetaLog.error(getClass(), e); + } + } + if (conn != null) { + try { + c.close(); + } + catch (IOException | TimeoutException e) { + MetaLog.error(getClass(), e); + } + } + this.channel = null; + this.connection = null; + } + finally { + lock.writeLock().unlock(); + } + + } + +} diff --git a/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/package-info.java b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/package-info.java new file mode 100644 index 00000000..9ae118cf --- /dev/null +++ b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/package-info.java @@ -0,0 +1,2 @@ +@org.eclipse.jdt.annotation.NonNullByDefault +package io.jstach.rainbowgum.rabbitmq; \ No newline at end of file diff --git a/test/pom.xml b/test/pom.xml index 59ba4685..11421fa4 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -17,6 +17,7 @@ rainbowgum-test-avaje + rainbowgum-test-config diff --git a/test/rainbowgum-test-config/pom.xml b/test/rainbowgum-test-config/pom.xml new file mode 100644 index 00000000..583a541f --- /dev/null +++ b/test/rainbowgum-test-config/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + io.jstach.rainbowgum + rainbowgum-test-parent + 0.1.3-SNAPSHOT + + rainbowgum-test-config + + + io.jstach.rainbowgum + rainbowgum-core + true + provided + + + io.jstach.rainbowgum + rainbowgum-config-apt + true + provided + + + \ No newline at end of file diff --git a/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java b/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java new file mode 100644 index 00000000..e8a7643c --- /dev/null +++ b/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java @@ -0,0 +1,15 @@ +package io.jstach.rainbowgum.test.config; + +import java.net.URI; + +import org.eclipse.jdt.annotation.Nullable; + +import io.jstach.rainbowgum.ConfigObject; + +public record ExampleConfig(String name, Integer count, @Nullable URI uri) { + + @ConfigObject(name = "ExampleConfigBuilder", prefix = "logging.example.") + public static ExampleConfig of(String name, Integer count, @Nullable URI uri) { + return new ExampleConfig(name, count, uri); + } +} diff --git a/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/package-info.java b/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/package-info.java new file mode 100644 index 00000000..099b210a --- /dev/null +++ b/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/package-info.java @@ -0,0 +1,2 @@ +@org.eclipse.jdt.annotation.NonNullByDefault +package io.jstach.rainbowgum.test.config; \ No newline at end of file