io.micronaut.pulsar.PulsarReaderFactory Maven / Gradle / Ivy
/*
* Copyright 2017-2022 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.pulsar;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.AbstractBeanResolutionContext;
import io.micronaut.context.BeanResolutionContext;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.context.annotation.Prototype;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.type.Argument;
import io.micronaut.inject.ArgumentInjectionPoint;
import io.micronaut.inject.FieldInjectionPoint;
import io.micronaut.inject.InjectionPoint;
import io.micronaut.pulsar.annotation.PulsarReader;
import io.micronaut.pulsar.processor.DefaultSchemaHandler;
import io.micronaut.pulsar.processor.TopicResolver;
import org.apache.pulsar.client.api.*;
import org.apache.pulsar.common.schema.KeyValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Create pulsar reader beans for fields annotated with PulsarReader.
*
* @author Haris Secic
* @since 1.0
*/
@Factory
public class PulsarReaderFactory implements AutoCloseable, PulsarReaderRegistry {
private static final Logger LOG = LoggerFactory.getLogger(PulsarReaderFactory.class);
private final Map> readers = new ConcurrentHashMap<>();
private final PulsarClient pulsarClient;
private final DefaultSchemaHandler simpleSchemaResolver;
private final TopicResolver topicResolver;
public PulsarReaderFactory(final PulsarClient pulsarClient,
final DefaultSchemaHandler simpleSchemaResolver,
final TopicResolver topicResolver) {
this.pulsarClient = pulsarClient;
this.simpleSchemaResolver = simpleSchemaResolver;
this.topicResolver = topicResolver;
}
/**
* Create Pulsar Reader for given injection point if missing.
*
* @param context injection point of {@code Reader>} if used as injection argument.
* @param annotationValue method annotation value if used by annotating method
* @param methodInvocationContext method invocation context if used by annotating method
* @param returnType method return type if used by annotating method
* @return new instance of Pulsar reader if missing; otherwise return from cache
* @throws PulsarClientException in case of not being able to create such Reader
*/
@Prototype
public Reader> getReaderByInjectionPoint(final BeanResolutionContext context,
@Nullable @Parameter final AnnotationValue annotationValue,
@Nullable @Parameter final Argument> returnType,
@Nullable @Parameter final MethodInvocationContext, ?> methodInvocationContext)
throws PulsarClientException {
if (context.getPath().currentSegment().isEmpty()) {
return getReaderForAnnotation(Objects.requireNonNull(annotationValue),
Objects.requireNonNull(returnType),
Objects.requireNonNull(methodInvocationContext));
}
return getReaderByInjectionPoint(context);
}
private Reader> getReaderByInjectionPoint(final BeanResolutionContext context) throws PulsarClientException {
final InjectionPoint> injectionPoint = context.getPath().currentSegment()
.orElseThrow(() ->
new IllegalStateException(
"Could not resolve current injection context while creating a reader"))
.getInjectionPoint();
final var annotation = injectionPoint.getAnnotation(PulsarReader.class);
if (null == annotation) {
throw new IllegalStateException("Failed to get value for bean annotated with PulsarReader");
}
final String topicValue = annotation.getRequiredValue(String.class);
final Argument> readerArgument;
final String declaredName;
final String target;
if (injectionPoint instanceof ArgumentInjectionPoint, ?> argumentInjection) {
readerArgument = argumentInjection.getArgument().getFirstTypeVariable()
.orElse(Argument.of(byte[].class));
declaredName = argumentInjection.getArgument().getName();
target = argumentInjection.getDeclaringBean().getName() + " " + declaredName;
if (argumentInjection instanceof AbstractBeanResolutionContext.ConstructorArgumentSegment
&& TopicResolver.isDynamicTenantInTopic(topicValue)) {
throw new ConfigurationException(String.format(
"Cannot use dynamic tenant in topics for constructor injected Readers in %s",
target
));
}
} else if (injectionPoint instanceof FieldInjectionPoint, ?> fieldInjection) {
readerArgument = fieldInjection.asArgument()
.getFirstTypeVariable()
.orElse(Argument.of(byte[].class));
declaredName = fieldInjection.getName();
target = fieldInjection.getDeclaringBean().getName() + "::" + declaredName;
if (TopicResolver.isDynamicTenantInTopic(topicValue)) {
throw new ConfigurationException(String.format(
"Cannot use dynamic tenant in topics for field injected Readers in %s",
target
));
}
} else {
readerArgument = Argument.of(byte[].class);
declaredName = injectionPoint.getDeclaringBean().getName();
target = declaredName;
if (TopicResolver.isDynamicTenantInTopic(topicValue)) {
throw new ConfigurationException(String.format(
"Cannot use dynamic tenant in topics for field injected Readers in %s",
target
));
}
}
return getOrCreateReader(annotation, readerArgument, declaredName, target);
}
/**
* Micronaut has issues with having BeanContext injected with @Primary for one method and
* second @Prototype for non injection context - for method annotations. Even @Named annotation
* won't help since beanContext.createBean will throw "NoSuchBean". For this reason check in 1
* method all parameters and decide to switch to this creator if necessary.
*/
private Reader> getReaderForAnnotation(final AnnotationValue annotationValue,
final Argument> returnType,
final MethodInvocationContext, ?> methodInvocationContext)
throws PulsarClientException {
final String target = methodInvocationContext
.getExecutableMethod()
.getDescription(false);
final String declaredName = methodInvocationContext.getExecutableMethod().getName();
return getOrCreateReader(annotationValue, returnType, declaredName, target);
}
private Reader> getOrCreateReader(final AnnotationValue annotation,
final Argument> readerArgument,
final String declaredName,
final String target) throws PulsarClientException {
final Argument> keyClass;
final Argument> messageBodyType;
if (KeyValue.class.isAssignableFrom(readerArgument.getType())) {
keyClass = readerArgument.getTypeParameters()[0];
messageBodyType = readerArgument.getTypeParameters()[1];
} else {
if (Message.class.isAssignableFrom(readerArgument.getType())) {
messageBodyType = readerArgument.getFirstTypeVariable().orElseThrow(() ->
new ConfigurationException("Reader methods must return non-raw Message"));
} else {
messageBodyType = readerArgument;
}
keyClass = null;
}
final var name = annotation.stringValue("readerName").orElse(declaredName);
final var topicResolved = TopicResolver.extractTopic(annotation, name);
final var readerId = topicResolver.generateIdFromMessagingClientName(name, topicResolved);
if (readers.containsKey(readerId)) {
return readers.get(readerId);
}
final var schema = simpleSchemaResolver.decideSchema(messageBodyType,
keyClass,
annotation,
target);
final var topic = topicResolver.resolve(topicResolved.getTopic());
final MessageId startMessageId;
if (annotation.getRequiredValue("startMessageLatest", boolean.class)) {
startMessageId = MessageId.latest;
} else {
startMessageId = MessageId.earliest;
}
final var subscriptionName = annotation.stringValue("subscriptionName");
final var readerBuilder = pulsarClient.newReader(schema)
.startMessageId(startMessageId)
.readerName(readerId)
.topic(topic);
subscriptionName.ifPresent(readerBuilder::subscriptionName);
final var reader = readerBuilder.create();
readers.put(readerId, reader);
return reader;
}
@Override
public void close() {
for (final Reader> reader : readers.values()) {
try {
reader.close();
} catch (Exception e) {
LOG.warn("Error shutting down Pulsar reader: {}", e.getMessage(), e);
}
}
readers.clear();
}
@Override
public Reader> getReader(final String identifier) {
return readers.get(identifier);
}
@Override
public Collection> getReaders() {
return readers.values();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy