GH-7: Add checkstyle and javaformat plugins
Fixes: #7 * Run `./gradlew format` * Updates from PR review suggestions
This commit is contained in:
@@ -63,17 +63,11 @@ public class AggregatorFunctionConfiguration {
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
@Bean
|
||||
public Function<Flux<Message<?>>, Flux<Message<?>>> aggregatorFunction(
|
||||
FluxMessageChannel inputChannel,
|
||||
FluxMessageChannel outputChannel
|
||||
) {
|
||||
public Function<Flux<Message<?>>, Flux<Message<?>>> aggregatorFunction(FluxMessageChannel inputChannel,
|
||||
FluxMessageChannel outputChannel) {
|
||||
return input -> Flux.from(outputChannel)
|
||||
.doOnRequest((request) ->
|
||||
inputChannel.subscribeTo(
|
||||
input.map((inputMessage) ->
|
||||
MessageBuilder.fromMessage(inputMessage)
|
||||
.removeHeader("kafka_consumer")
|
||||
.build())));
|
||||
.doOnRequest((request) -> inputChannel.subscribeTo(input.map((
|
||||
inputMessage) -> MessageBuilder.fromMessage(inputMessage).removeHeader("kafka_consumer").build())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -88,13 +82,10 @@ public class AggregatorFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
@ServiceActivator(inputChannel = "inputChannel")
|
||||
public AggregatorFactoryBean aggregator(
|
||||
@Nullable CorrelationStrategy correlationStrategy,
|
||||
@Nullable ReleaseStrategy releaseStrategy,
|
||||
@Nullable MessageGroupProcessor messageGroupProcessor,
|
||||
@Nullable MessageGroupStore messageStore,
|
||||
@Qualifier("outputChannel") MessageChannel outputChannel,
|
||||
@Nullable ComponentCustomizer<AggregatorFactoryBean> aggregatorCustomizer) {
|
||||
public AggregatorFactoryBean aggregator(@Nullable CorrelationStrategy correlationStrategy,
|
||||
@Nullable ReleaseStrategy releaseStrategy, @Nullable MessageGroupProcessor messageGroupProcessor,
|
||||
@Nullable MessageGroupStore messageStore, @Qualifier("outputChannel") MessageChannel outputChannel,
|
||||
@Nullable ComponentCustomizer<AggregatorFactoryBean> aggregatorCustomizer) {
|
||||
|
||||
AggregatorFactoryBean aggregator = new AggregatorFactoryBean();
|
||||
aggregator.setExpireGroupsUponCompletion(true);
|
||||
@@ -149,14 +140,10 @@ public class AggregatorFunctionConfiguration {
|
||||
return new ExpressionEvaluatingMessageGroupProcessor(this.properties.getAggregation().getExpressionString());
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnMissingBean(MessageGroupStore.class)
|
||||
@Import({
|
||||
MessageStoreConfiguration.Mongo.class,
|
||||
MessageStoreConfiguration.Redis.class,
|
||||
MessageStoreConfiguration.Jdbc.class
|
||||
})
|
||||
@Import({ MessageStoreConfiguration.Mongo.class, MessageStoreConfiguration.Redis.class,
|
||||
MessageStoreConfiguration.Jdbc.class })
|
||||
protected static class MessageStoreAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ public class AggregatorFunctionProperties {
|
||||
private String messageStoreType = MessageStoreType.SIMPLE;
|
||||
|
||||
/**
|
||||
* Persistence message store entity: table prefix in RDBMS, collection name in MongoDb, etc.
|
||||
* Persistence message store entity: table prefix in RDBMS, collection name in
|
||||
* MongoDb, etc.
|
||||
*/
|
||||
private String messageStoreEntity;
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
|
||||
/**
|
||||
* An {@link EnvironmentPostProcessor} to add {@code spring.autoconfigure.exclude} property
|
||||
* since we can't use {@code application.properties} from the library perspective.
|
||||
* An {@link EnvironmentPostProcessor} to add {@code spring.autoconfigure.exclude}
|
||||
* property since we can't use {@code application.properties} from the library
|
||||
* perspective.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @author Corneil du Plessis
|
||||
@@ -45,16 +46,14 @@ public class ExcludeStoresAutoConfigurationEnvironmentPostProcessor implements E
|
||||
MutablePropertySources propertySources = environment.getPropertySources();
|
||||
Properties properties = new Properties();
|
||||
|
||||
properties.setProperty("spring.autoconfigure.exclude",
|
||||
DataSourceAutoConfiguration.class.getName() + ", " +
|
||||
DataSourceTransactionManagerAutoConfiguration.class.getName() + ", " +
|
||||
MongoAutoConfiguration.class.getName() + ", " +
|
||||
MongoDataAutoConfiguration.class.getName() + ", " +
|
||||
MongoRepositoriesAutoConfiguration.class.getName() + ", " +
|
||||
RedisAutoConfiguration.class.getName() + ", " +
|
||||
RedisRepositoriesAutoConfiguration.class.getName());
|
||||
properties.setProperty("spring.autoconfigure.exclude", DataSourceAutoConfiguration.class.getName() + ", "
|
||||
+ DataSourceTransactionManagerAutoConfiguration.class.getName() + ", "
|
||||
+ MongoAutoConfiguration.class.getName() + ", " + MongoDataAutoConfiguration.class.getName() + ", "
|
||||
+ MongoRepositoriesAutoConfiguration.class.getName() + ", " + RedisAutoConfiguration.class.getName()
|
||||
+ ", " + RedisRepositoriesAutoConfiguration.class.getName());
|
||||
|
||||
propertySources.addLast(new PropertiesPropertySource("aggregator.exclude.stores.auto-configuration", properties));
|
||||
propertySources
|
||||
.addLast(new PropertiesPropertySource("aggregator.exclude.stores.auto-configuration", properties));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,11 +40,10 @@ import org.springframework.integration.store.MessageGroupStore;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
||||
/**
|
||||
* A helper class containing configuration classes for particular technologies
|
||||
* to expose an appropriate {@link org.springframework.integration.store.MessageStore} bean
|
||||
* via matched configuration properties.
|
||||
* A helper class containing configuration classes for particular technologies to expose
|
||||
* an appropriate {@link org.springframework.integration.store.MessageStore} bean via
|
||||
* matched configuration properties.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @author Corneil du Plessis
|
||||
@@ -52,11 +51,9 @@ import org.springframework.util.StringUtils;
|
||||
class MessageStoreConfiguration {
|
||||
|
||||
@ConditionalOnClass(ConfigurableMongoDbMessageStore.class)
|
||||
@ConditionalOnProperty(prefix = AggregatorFunctionProperties.PREFIX,
|
||||
name = "message-store-type",
|
||||
@ConditionalOnProperty(prefix = AggregatorFunctionProperties.PREFIX, name = "message-store-type",
|
||||
havingValue = AggregatorFunctionProperties.MessageStoreType.MONGODB)
|
||||
@Import({ MongoAutoConfiguration.class,
|
||||
MongoDataAutoConfiguration.class })
|
||||
@Import({ MongoAutoConfiguration.class, MongoDataAutoConfiguration.class })
|
||||
static class Mongo {
|
||||
|
||||
@Bean
|
||||
@@ -72,15 +69,14 @@ class MessageStoreConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
public MongoCustomConversions mongoDbCustomConversions() {
|
||||
return new MongoCustomConversions(Arrays.asList(
|
||||
new MessageToBinaryConverter(), new BinaryToMessageConverter()));
|
||||
return new MongoCustomConversions(
|
||||
Arrays.asList(new MessageToBinaryConverter(), new BinaryToMessageConverter()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnClass(RedisMessageStore.class)
|
||||
@ConditionalOnProperty(prefix = AggregatorFunctionProperties.PREFIX,
|
||||
name = "message-store-type",
|
||||
@ConditionalOnProperty(prefix = AggregatorFunctionProperties.PREFIX, name = "message-store-type",
|
||||
havingValue = AggregatorFunctionProperties.MessageStoreType.REDIS)
|
||||
@Import(RedisAutoConfiguration.class)
|
||||
static class Redis {
|
||||
@@ -93,12 +89,9 @@ class MessageStoreConfiguration {
|
||||
}
|
||||
|
||||
@ConditionalOnClass(JdbcMessageStore.class)
|
||||
@ConditionalOnProperty(prefix = AggregatorFunctionProperties.PREFIX,
|
||||
name = "message-store-type",
|
||||
@ConditionalOnProperty(prefix = AggregatorFunctionProperties.PREFIX, name = "message-store-type",
|
||||
havingValue = AggregatorFunctionProperties.MessageStoreType.JDBC)
|
||||
@Import({
|
||||
DataSourceAutoConfiguration.class,
|
||||
DataSourceTransactionManagerAutoConfiguration.class })
|
||||
@Import({ DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
|
||||
static class Jdbc {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -48,9 +48,11 @@ public abstract class AbstractAggregatorFunctionTests {
|
||||
|
||||
@SpringBootApplication
|
||||
public static class AggregatorFunctionTestApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AggregatorFunctionTestApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,13 +38,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
@TestPropertySource(properties = {
|
||||
"aggregator.correlation=T(Thread).currentThread().id",
|
||||
@TestPropertySource(properties = { "aggregator.correlation=T(Thread).currentThread().id",
|
||||
"aggregator.release=!messages.?[payload == 'bar'].empty",
|
||||
"aggregator.aggregation=#this.?[payload == 'foo'].![payload]",
|
||||
"aggregator.messageStoreType=mongodb",
|
||||
"aggregator.message-store-entity=aggregatorTest"
|
||||
})
|
||||
"aggregator.aggregation=#this.?[payload == 'foo'].![payload]", "aggregator.messageStoreType=mongodb",
|
||||
"aggregator.message-store-entity=aggregatorTest" })
|
||||
@AutoConfigureDataMongo
|
||||
public class CustomPropsAndMongoMessageStoreAggregatorTests extends AbstractAggregatorFunctionTests
|
||||
implements MongoDbTestContainerSupport {
|
||||
@@ -57,22 +54,19 @@ public class CustomPropsAndMongoMessageStoreAggregatorTests extends AbstractAggr
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Flux<Message<?>> input =
|
||||
Flux.just("foo", "bar")
|
||||
.map(GenericMessage::new);
|
||||
Flux<Message<?>> input = Flux.just("foo", "bar").map(GenericMessage::new);
|
||||
|
||||
Flux<Message<?>> output = this.aggregatorFunction.apply(input);
|
||||
|
||||
output.as(StepVerifier::create)
|
||||
.assertNext((message) ->
|
||||
assertThat(message)
|
||||
.extracting(Message::getPayload)
|
||||
.isInstanceOf(List.class)
|
||||
.asList()
|
||||
.hasSize(1)
|
||||
.element(0).isEqualTo("foo"))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(10));
|
||||
.assertNext((message) -> assertThat(message).extracting(Message::getPayload)
|
||||
.isInstanceOf(List.class)
|
||||
.asList()
|
||||
.hasSize(1)
|
||||
.element(0)
|
||||
.isEqualTo("foo"))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(10));
|
||||
|
||||
assertThat(this.messageGroupStore).isInstanceOf(ConfigurableMongoDbMessageStore.class);
|
||||
assertThat(TestUtils.getPropertyValue(this.messageGroupStore, "collectionName")).isEqualTo("aggregatorTest");
|
||||
|
||||
@@ -40,12 +40,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@Disabled("Fails on CI sporadically")
|
||||
@TestPropertySource(properties = "aggregator.message-store-type=simple")
|
||||
public class DefaultAggregatorTests extends AbstractAggregatorFunctionTests {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultAggregatorTests.class);
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Flux<Message<?>> input =
|
||||
Flux.just(MessageBuilder.withPayload("2")
|
||||
Flux<Message<?>> input = Flux.just(
|
||||
MessageBuilder.withPayload("2")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "my_correlation")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 2)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 2)
|
||||
@@ -57,20 +58,13 @@ public class DefaultAggregatorTests extends AbstractAggregatorFunctionTests {
|
||||
.build());
|
||||
|
||||
Flux<Message<?>> output = this.aggregatorFunction.apply(input.log("DefaultAggregatorTests:input"));
|
||||
output.log("DefaultAggregatorTests:output")
|
||||
.as(StepVerifier::create)
|
||||
.assertNext((message) -> {
|
||||
assertThat(message)
|
||||
.extracting(Message::getPayload)
|
||||
.asList()
|
||||
.hasSize(2)
|
||||
.contains("1", "2");
|
||||
})
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(30));
|
||||
output.log("DefaultAggregatorTests:output").as(StepVerifier::create).assertNext((message) -> {
|
||||
assertThat(message).extracting(Message::getPayload).asList().hasSize(2).contains("1", "2");
|
||||
}).thenCancel().verify(Duration.ofSeconds(30));
|
||||
|
||||
assertThat(this.messageGroupStore).isNull();
|
||||
assertThat(this.aggregatingMessageHandler.getMessageStore()).isInstanceOf(SimpleMessageStore.class);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,30 +40,28 @@ public class JdbcMessageStoreAggregatorTests extends AbstractAggregatorFunctionT
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Flux<Message<?>> input =
|
||||
Flux.just(MessageBuilder.withPayload("2")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "my_correlation")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 2)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 2)
|
||||
.build(),
|
||||
MessageBuilder.withPayload("1")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "my_correlation")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 1)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 2)
|
||||
.build());
|
||||
Flux<Message<?>> input = Flux.just(
|
||||
MessageBuilder.withPayload("2")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "my_correlation")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 2)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 2)
|
||||
.build(),
|
||||
MessageBuilder.withPayload("1")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "my_correlation")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 1)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 2)
|
||||
.build());
|
||||
|
||||
Flux<Message<?>> output = this.aggregatorFunction.apply(input);
|
||||
|
||||
output.as(StepVerifier::create)
|
||||
.assertNext((message) ->
|
||||
assertThat(message)
|
||||
.extracting(Message::getPayload)
|
||||
.isInstanceOf(List.class)
|
||||
.asList()
|
||||
.hasSize(2)
|
||||
.contains("1", "2"))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(10));
|
||||
.assertNext((message) -> assertThat(message).extracting(Message::getPayload)
|
||||
.isInstanceOf(List.class)
|
||||
.asList()
|
||||
.hasSize(2)
|
||||
.contains("1", "2"))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(10));
|
||||
|
||||
assertThat(this.messageGroupStore).isInstanceOf(JdbcMessageStore.class);
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
@TestPropertySource(properties = "aggregator.message-store-type=redis")
|
||||
public class RedisMessageStoreAggregatorTests extends AbstractAggregatorFunctionTests implements RedisTestContainerSupport {
|
||||
public class RedisMessageStoreAggregatorTests extends AbstractAggregatorFunctionTests
|
||||
implements RedisTestContainerSupport {
|
||||
|
||||
@DynamicPropertySource
|
||||
static void redisProperties(DynamicPropertyRegistry registry) {
|
||||
@@ -52,8 +53,8 @@ public class RedisMessageStoreAggregatorTests extends AbstractAggregatorFunction
|
||||
public void test() {
|
||||
InputStream fakeNonSerializableKafkaConsumer = new ByteArrayInputStream(new byte[0]);
|
||||
|
||||
Flux<Message<?>> input =
|
||||
Flux.just(MessageBuilder.withPayload("2")
|
||||
Flux<Message<?>> input = Flux.just(
|
||||
MessageBuilder.withPayload("2")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "my_correlation")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 2)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 2)
|
||||
@@ -68,13 +69,11 @@ public class RedisMessageStoreAggregatorTests extends AbstractAggregatorFunction
|
||||
Flux<Message<?>> output = this.aggregatorFunction.apply(input);
|
||||
|
||||
output.as(StepVerifier::create)
|
||||
.assertNext((message) ->
|
||||
assertThat(message)
|
||||
.extracting(Message::getPayload)
|
||||
.isInstanceOf(List.class)
|
||||
.asList()
|
||||
.hasSize(2)
|
||||
.contains("1", "2"))
|
||||
.assertNext((message) -> assertThat(message).extracting(Message::getPayload)
|
||||
.isInstanceOf(List.class)
|
||||
.asList()
|
||||
.hasSize(2)
|
||||
.contains("1", "2"))
|
||||
.thenCancel()
|
||||
.verify(Duration.ofSeconds(10));
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public class FilterFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, Message<?>> filterFunction(
|
||||
ExpressionEvaluatingTransformer filterExpressionEvaluatingTransformer) {
|
||||
ExpressionEvaluatingTransformer filterExpressionEvaluatingTransformer) {
|
||||
|
||||
return message -> {
|
||||
if ((Boolean) filterExpressionEvaluatingTransformer.transform(message).getPayload()) {
|
||||
@@ -48,7 +48,7 @@ public class FilterFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public ExpressionEvaluatingTransformer filterExpressionEvaluatingTransformer(
|
||||
FilterFunctionProperties filterFunctionProperties) {
|
||||
FilterFunctionProperties filterFunctionProperties) {
|
||||
|
||||
return new ExpressionEvaluatingTransformer(filterFunctionProperties.getExpression());
|
||||
}
|
||||
|
||||
@@ -21,11 +21,8 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -50,10 +47,9 @@ public class FilterFunctionApplicationTests {
|
||||
|
||||
@Test
|
||||
public void testFilter() {
|
||||
Stream<Message<?>> messages = List.of("hello", "hello world")
|
||||
.stream()
|
||||
.map(GenericMessage::new);
|
||||
List<Message<?>> result = messages.filter(message -> this.filter.apply(message) != null).collect(Collectors.toList());
|
||||
Stream<Message<?>> messages = List.of("hello", "hello world").stream().map(GenericMessage::new);
|
||||
List<Message<?>> result = messages.filter(message -> this.filter.apply(message) != null)
|
||||
.collect(Collectors.toList());
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
assertThat(result.get(0).getPayload()).isNotNull();
|
||||
assertThat(result.get(0).getPayload()).isEqualTo("hello world");
|
||||
|
||||
@@ -58,8 +58,8 @@ public class HeaderEnricherFunctionConfiguration {
|
||||
Enumeration<?> enumeration = props.propertyNames();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
String propertyName = (String) enumeration.nextElement();
|
||||
ExpressionEvaluatingHeaderValueMessageProcessor<?> headerValueMessageProcessor =
|
||||
new ExpressionEvaluatingHeaderValueMessageProcessor<>(props.getProperty(propertyName), null);
|
||||
ExpressionEvaluatingHeaderValueMessageProcessor<?> headerValueMessageProcessor = new ExpressionEvaluatingHeaderValueMessageProcessor<>(
|
||||
props.getProperty(propertyName), null);
|
||||
headerValueMessageProcessor.setBeanFactory(beanFactory);
|
||||
headersToAdd.put(propertyName, headerValueMessageProcessor);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ import org.springframework.validation.annotation.Validated;
|
||||
public class HeaderEnricherFunctionProperties {
|
||||
|
||||
/**
|
||||
* \n separated properties representing headers in which values are SpEL expressions, e.g
|
||||
* foo='bar' \n baz=payload.baz.
|
||||
* \n separated properties representing headers in which values are SpEL expressions,
|
||||
* e.g foo='bar' \n baz=payload.baz.
|
||||
*/
|
||||
private Properties headers;
|
||||
|
||||
|
||||
@@ -33,12 +33,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
@SpringBootTest(properties = {
|
||||
"header.enricher.headers=foo='bar' \\n baz='fiz' \\n buz=payload \\n jaz=@value",
|
||||
@SpringBootTest(properties = { "header.enricher.headers=foo='bar' \\n baz='fiz' \\n buz=payload \\n jaz=@value",
|
||||
"header.enricher.overwrite = true" })
|
||||
@DirtiesContext
|
||||
public class HeaderEnricherFunctionApplicationTests {
|
||||
@@ -48,8 +46,7 @@ public class HeaderEnricherFunctionApplicationTests {
|
||||
|
||||
@Test
|
||||
public void testDefault() {
|
||||
final Message<?> message = MessageBuilder.withPayload("hello")
|
||||
.setHeader("baz", "qux").build();
|
||||
final Message<?> message = MessageBuilder.withPayload("hello").setHeader("baz", "qux").build();
|
||||
final Message<?> enriched = headerEnricherFunction.apply(message);
|
||||
|
||||
assertThat(enriched, HeaderMatcher.hasHeader("foo", equalTo("bar")));
|
||||
|
||||
@@ -37,7 +37,9 @@ import org.springframework.util.StringUtils;
|
||||
@EnableConfigurationProperties(HeaderFilterFunctionProperties.class)
|
||||
@ConditionalOnExpression("'${header.filter.remove}'!='' or '${header.filter.delete-all}' != ''")
|
||||
public class HeaderFilterFunctionConfiguration {
|
||||
|
||||
private final HeaderFilterFunctionProperties properties;
|
||||
|
||||
public HeaderFilterFunctionConfiguration(HeaderFilterFunctionProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
@@ -46,7 +48,7 @@ public class HeaderFilterFunctionConfiguration {
|
||||
public Function<Message<?>, Message<?>> headerFilterFunction() {
|
||||
if (properties.isDeleteAll()) {
|
||||
return (message) -> {
|
||||
var accessor = new IntegrationMessageHeaderAccessor(message);
|
||||
var accessor = new IntegrationMessageHeaderAccessor(message);
|
||||
var headers = new HashSet<>(message.getHeaders().keySet());
|
||||
headers.removeIf(accessor::isReadOnly);
|
||||
HeaderFilter filter = new HeaderFilter(headers.toArray(new String[0]));
|
||||
|
||||
@@ -20,18 +20,20 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Properties for configuration of header-filter-function.
|
||||
*
|
||||
* @author Corneil du Plessis
|
||||
*/
|
||||
@ConfigurationProperties("header.filter")
|
||||
public class HeaderFilterFunctionProperties {
|
||||
|
||||
/**
|
||||
* Indicates the need to remove all headers.
|
||||
*/
|
||||
private boolean deleteAll = false;
|
||||
|
||||
/**
|
||||
* Remove all headers named. A comma, space separated list of header names.
|
||||
* The names may contain patterns.
|
||||
* Remove all headers named. A comma, space separated list of header names. The names
|
||||
* may contain patterns.
|
||||
*/
|
||||
private String remove;
|
||||
|
||||
@@ -50,4 +52,5 @@ public class HeaderFilterFunctionProperties {
|
||||
public void setRemove(String remove) {
|
||||
this.remove = remove;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,13 +30,10 @@ import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(classes = {
|
||||
HeaderFilterFunctionApplicationDeleteAllTests.HeaderFilterFunctionTestApplication.class,
|
||||
HeaderFilterFunctionConfiguration.class
|
||||
},
|
||||
properties = {"header.filter.delete-all=true"}
|
||||
)
|
||||
@SpringBootTest(classes = { HeaderFilterFunctionApplicationDeleteAllTests.HeaderFilterFunctionTestApplication.class,
|
||||
HeaderFilterFunctionConfiguration.class }, properties = { "header.filter.delete-all=true" })
|
||||
public class HeaderFilterFunctionApplicationDeleteAllTests {
|
||||
|
||||
@Autowired
|
||||
protected Function<Message<?>, Message<?>> headerFilter;
|
||||
|
||||
@@ -54,9 +51,11 @@ public class HeaderFilterFunctionApplicationDeleteAllTests {
|
||||
|
||||
@SpringBootApplication
|
||||
static class HeaderFilterFunctionTestApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SpringApplication.main(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,13 +29,10 @@ import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(classes = {
|
||||
HeaderFilterFunctionApplicationTests.HeaderFilterFunctionTestApplication.class,
|
||||
HeaderFilterFunctionConfiguration.class
|
||||
},
|
||||
properties = {"header.filter.remove=foo,bar,pf-*"}
|
||||
)
|
||||
@SpringBootTest(classes = { HeaderFilterFunctionApplicationTests.HeaderFilterFunctionTestApplication.class,
|
||||
HeaderFilterFunctionConfiguration.class }, properties = { "header.filter.remove=foo,bar,pf-*" })
|
||||
public class HeaderFilterFunctionApplicationTests {
|
||||
|
||||
@Autowired
|
||||
protected Function<Message<?>, Message<?>> headerFilter;
|
||||
|
||||
@@ -93,9 +90,11 @@ public class HeaderFilterFunctionApplicationTests {
|
||||
|
||||
@SpringBootApplication
|
||||
static class HeaderFilterFunctionTestApplication {
|
||||
|
||||
public static void main(String[] main) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
final public class HeaderUtils {
|
||||
|
||||
private HeaderUtils() {
|
||||
}
|
||||
|
||||
@@ -35,4 +36,5 @@ final public class HeaderUtils {
|
||||
headers.removeIf(accessor::isReadOnly);
|
||||
return headers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Configuration for a {@link Function} that makes HTTP requests to a resource and for
|
||||
* each request, returns a {@link ResponseEntity}.
|
||||
@@ -47,7 +46,8 @@ import org.springframework.web.util.UriBuilderFactory;
|
||||
public class HttpRequestFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
public HttpRequestFunction httpRequestFunction(WebClient.Builder webClientBuilder, HttpRequestFunctionProperties properties) {
|
||||
public HttpRequestFunction httpRequestFunction(WebClient.Builder webClientBuilder,
|
||||
HttpRequestFunctionProperties properties) {
|
||||
return new HttpRequestFunction(webClientBuilder.build(), properties);
|
||||
}
|
||||
|
||||
@@ -76,8 +76,7 @@ public class HttpRequestFunctionConfiguration {
|
||||
|
||||
@Override
|
||||
public Object apply(Message<?> message) {
|
||||
return this.webClient
|
||||
.method(resolveHttpMethod(message))
|
||||
return this.webClient.method(resolveHttpMethod(message))
|
||||
.uri(uriBuilderFactory.uriString(resolveUrl(message)).build())
|
||||
.bodyValue(resolveBody(message))
|
||||
.headers(httpHeaders -> httpHeaders.addAll(resolveHeaders(message)))
|
||||
@@ -98,7 +97,7 @@ public class HttpRequestFunctionConfiguration {
|
||||
|
||||
private Object resolveBody(Message<?> message) {
|
||||
return properties.getBodyExpression() != null ? properties.getBodyExpression().getValue(message)
|
||||
: message.getPayload();
|
||||
: message.getPayload();
|
||||
}
|
||||
|
||||
private HttpHeaders resolveHeaders(Message<?> message) {
|
||||
@@ -107,8 +106,7 @@ public class HttpRequestFunctionConfiguration {
|
||||
Map<?, ?> headersMap = properties.getHeadersExpression().getValue(message, Map.class);
|
||||
for (Map.Entry<?, ?> header : headersMap.entrySet()) {
|
||||
if (header.getKey() != null && header.getValue() != null) {
|
||||
headers.add(header.getKey().toString(),
|
||||
header.getValue().toString());
|
||||
headers.add(header.getKey().toString(), header.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,5 +121,7 @@ public class HttpRequestFunctionConfiguration {
|
||||
public HttpMethod convert(String source) {
|
||||
return HttpMethod.valueOf(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,25 +43,21 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
public class HttpRequestFunctionTestApplicationTests {
|
||||
|
||||
private MockWebServer server = new MockWebServer();
|
||||
|
||||
private ApplicationContextRunner runner;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.runner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(HttpRequestFunctionTestApplication.class)
|
||||
.withPropertyValues(
|
||||
"http.request.reply-expression=#root",
|
||||
"http.request.url-expression='" + url() + "'");
|
||||
this.runner = new ApplicationContextRunner().withUserConfiguration(HttpRequestFunctionTestApplication.class)
|
||||
.withPropertyValues("http.request.reply-expression=#root", "http.request.url-expression='" + url() + "'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnString() {
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(HttpStatus.OK.value())
|
||||
.setBody("hello"));
|
||||
server.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value()).setBody("hello"));
|
||||
|
||||
runner.withPropertyValues("http.request.http-method-expression='POST'").run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
@@ -78,26 +74,27 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
server.setDispatcher(new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest recordedRequest) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
|
||||
recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
return new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.CREATED.value());
|
||||
}
|
||||
});
|
||||
|
||||
runner.withPropertyValues("http.request.http-method-expression='POST'",
|
||||
"http.request.headers-expression={'Content-Type':'application/json'}").run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
String json = "{\"hello\":\"world\"}";
|
||||
Message<?> message = MessageBuilder.withPayload(json)
|
||||
.build();
|
||||
ResponseEntity r = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(r.getBody()).isEqualTo(json);
|
||||
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
});
|
||||
runner
|
||||
.withPropertyValues("http.request.http-method-expression='POST'",
|
||||
"http.request.headers-expression={'Content-Type':'application/json'}")
|
||||
.run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
String json = "{\"hello\":\"world\"}";
|
||||
Message<?> message = MessageBuilder.withPayload(json).build();
|
||||
ResponseEntity r = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(r.getBody()).isEqualTo(json);
|
||||
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,26 +103,27 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
server.setDispatcher(new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest recordedRequest) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
|
||||
recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
return new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.CREATED.value());
|
||||
}
|
||||
});
|
||||
|
||||
runner.withPropertyValues("http.request.http-method-expression='POST'",
|
||||
"http.request.expected-response-type=" + Map.class.getName()).run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
Map<String, String> json = Collections.singletonMap("hello", "world");
|
||||
Message<?> message = MessageBuilder.withPayload(json)
|
||||
.build();
|
||||
ResponseEntity r = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(r.getBody()).isEqualTo(json);
|
||||
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
});
|
||||
runner
|
||||
.withPropertyValues("http.request.http-method-expression='POST'",
|
||||
"http.request.expected-response-type=" + Map.class.getName())
|
||||
.run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
Map<String, String> json = Collections.singletonMap("hello", "world");
|
||||
Message<?> message = MessageBuilder.withPayload(json).build();
|
||||
ResponseEntity r = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(r.getBody()).isEqualTo(json);
|
||||
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,24 +131,25 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
server.setDispatcher(new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest recordedRequest) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
|
||||
recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.ACCEPTED.value());
|
||||
return new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.ACCEPTED.value());
|
||||
}
|
||||
});
|
||||
|
||||
runner.withPropertyValues("http.request.http-method-expression='DELETE'",
|
||||
"http.request.expected-response-type=" + Void.class.getName()).run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
Message<?> message = MessageBuilder.withPayload("")
|
||||
.build();
|
||||
ResponseEntity r = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(r.getBody()).isNull();
|
||||
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
|
||||
assertThat(request.getMethod()).isEqualTo("DELETE");
|
||||
});
|
||||
runner
|
||||
.withPropertyValues("http.request.http-method-expression='DELETE'",
|
||||
"http.request.expected-response-type=" + Void.class.getName())
|
||||
.run(context -> {
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
Message<?> message = MessageBuilder.withPayload("").build();
|
||||
ResponseEntity r = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(r.getBody()).isNull();
|
||||
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
|
||||
assertThat(request.getMethod()).isEqualTo("DELETE");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,17 +172,15 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
server.setDispatcher(new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest recordedRequest) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
|
||||
recordedRequest.getHeader(HttpHeaders.ACCEPT))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.OK.value());
|
||||
return new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, recordedRequest.getHeader(HttpHeaders.ACCEPT))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.OK.value());
|
||||
}
|
||||
});
|
||||
|
||||
runner.withPropertyValues(
|
||||
"http.request.url-expression=headers['url']",
|
||||
"http.request.http-method-expression=headers['method']",
|
||||
"http.request.body-expression=headers['body']",
|
||||
runner.withPropertyValues("http.request.url-expression=headers['url']",
|
||||
"http.request.http-method-expression=headers['method']", "http.request.body-expression=headers['body']",
|
||||
"http.request.headers-expression={Accept:'application/json'}")
|
||||
.run(context -> {
|
||||
Message<?> message = MessageBuilder.withPayload("")
|
||||
@@ -196,8 +193,7 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
ResponseEntity responseEntity = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(responseEntity.getBody()).isEqualTo(message.getHeaders().get("body"));
|
||||
assertThat(responseEntity.getHeaders().getContentType())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON);
|
||||
assertThat(responseEntity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,25 +202,23 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
server.setDispatcher(new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest recordedRequest) {
|
||||
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
|
||||
recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.OK.value());
|
||||
return new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setResponseCode(HttpStatus.OK.value());
|
||||
}
|
||||
});
|
||||
runner.withPropertyValues(
|
||||
"http.request..http-method-expression='POST'",
|
||||
"http.request.headers-expression={'Content-Type':'application/octet-stream'}",
|
||||
"http.request.expected-response-type=byte[]")
|
||||
runner
|
||||
.withPropertyValues("http.request..http-method-expression='POST'",
|
||||
"http.request.headers-expression={'Content-Type':'application/octet-stream'}",
|
||||
"http.request.expected-response-type=byte[]")
|
||||
.run(context -> {
|
||||
Message<?> message = MessageBuilder.withPayload("hello")
|
||||
.build();
|
||||
Message<?> message = MessageBuilder.withPayload("hello").build();
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
ResponseEntity responseEntity = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(responseEntity.getBody()).isEqualTo("hello".getBytes());
|
||||
assertThat(responseEntity.getHeaders().getContentType())
|
||||
.isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
|
||||
assertThat(responseEntity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,17 +227,14 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
server.setDispatcher(new Dispatcher() {
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest recordedRequest) {
|
||||
return new MockResponse()
|
||||
.setBody(recordedRequest.getBody())
|
||||
.setHeader("method", recordedRequest.getMethod())
|
||||
.setResponseCode(HttpStatus.OK.value());
|
||||
return new MockResponse().setBody(recordedRequest.getBody())
|
||||
.setHeader("method", recordedRequest.getMethod())
|
||||
.setResponseCode(HttpStatus.OK.value());
|
||||
}
|
||||
});
|
||||
runner.withPropertyValues(
|
||||
"http.request.http-method-expression=#jsonPath(payload,'$.myMethod')")
|
||||
runner.withPropertyValues("http.request.http-method-expression=#jsonPath(payload,'$.myMethod')")
|
||||
.run(context -> {
|
||||
Message<?> message = MessageBuilder
|
||||
.withPayload("{\"name\":\"Fred\",\"age\":41, \"myMethod\":\"POST\"}")
|
||||
Message<?> message = MessageBuilder.withPayload("{\"name\":\"Fred\",\"age\":41, \"myMethod\":\"POST\"}")
|
||||
.build();
|
||||
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
|
||||
ResponseEntity responseEntity = (ResponseEntity) httpRequestFunction.apply(message);
|
||||
@@ -258,5 +249,7 @@ public class HttpRequestFunctionTestApplicationTests {
|
||||
|
||||
@SpringBootApplication
|
||||
static class HttpRequestFunctionTestApplication {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,23 +46,29 @@ import org.springframework.util.StreamUtils;
|
||||
public class ImageRecognition implements AutoCloseable {
|
||||
|
||||
private final List<String> labels;
|
||||
|
||||
private final GraphRunner imageNormalization;
|
||||
|
||||
private final GraphRunner imageRecognition;
|
||||
|
||||
private final GraphRunner maxProbability;
|
||||
|
||||
private final GraphRunner topKProbabilities;
|
||||
|
||||
/**
|
||||
* Instead of creating the {@link ImageRecognition} service explicitly via the constructor,
|
||||
* you should consider the convenience factory methods below. E.g.
|
||||
*
|
||||
* {@link #inception(String, int, int, boolean)}
|
||||
* {@link #mobileNetV1(String, int, int, boolean)}
|
||||
* {@link #mobileNetV2(String, int, int, boolean)}
|
||||
* Instead of creating the {@link ImageRecognition} service explicitly via the
|
||||
* constructor, you should consider the convenience factory methods below. E.g.
|
||||
*
|
||||
* {@link #inception(String, int, int, boolean)}
|
||||
* {@link #mobileNetV1(String, int, int, boolean)}
|
||||
* {@link #mobileNetV2(String, int, int, boolean)}
|
||||
* @param modelUri location of the pre-trained model to use.
|
||||
* @param labelsUri location of the list fromMemory pre-trained categories used by the model.
|
||||
* @param imageRecognitionGraphInputName name of the Model's input node to send the input image to.
|
||||
* @param imageRecognitionGraphOutputName name of the Model's output node to retrieve the predictions from.
|
||||
* @param labelsUri location of the list fromMemory pre-trained categories used by the
|
||||
* model.
|
||||
* @param imageRecognitionGraphInputName name of the Model's input node to send the
|
||||
* input image to.
|
||||
* @param imageRecognitionGraphOutputName name of the Model's output node to retrieve
|
||||
* the predictions from.
|
||||
* @param imageHeight normalized image height.
|
||||
* @param imageWidth normalized image width.
|
||||
* @param mean mean value to normalize the input image.
|
||||
@@ -71,62 +77,66 @@ public class ImageRecognition implements AutoCloseable {
|
||||
* @param cacheModel if true the pre-trained model is cached on the local file system.
|
||||
*/
|
||||
public ImageRecognition(String modelUri, String labelsUri, int imageHeight, int imageWidth, float mean, float scale,
|
||||
String imageRecognitionGraphInputName, String imageRecognitionGraphOutputName, int responseSize, boolean cacheModel) {
|
||||
String imageRecognitionGraphInputName, String imageRecognitionGraphOutputName, int responseSize,
|
||||
boolean cacheModel) {
|
||||
|
||||
this.labels = labels(labelsUri);
|
||||
|
||||
/**
|
||||
* Normalizes the raw input image into format expected by the pre-trained Inception/MobileNetV1/MobileNetV2 models.
|
||||
* Typically the model is trained fromMemory images scaled to certain size. Usually it is 224x224 pixels, but can be
|
||||
* also 192x192, 160x160, 128128, 92x92. Use the (imageHeight, imageWidth) to set the desired size.
|
||||
* The colors, represented as R, G, B in 1-byte each were converted to float using (Value - Mean)/Scale.
|
||||
* Normalizes the raw input image into format expected by the pre-trained
|
||||
* Inception/MobileNetV1/MobileNetV2 models. Typically the model is trained
|
||||
* fromMemory images scaled to certain size. Usually it is 224x224 pixels, but can
|
||||
* be also 192x192, 160x160, 128128, 92x92. Use the (imageHeight, imageWidth) to
|
||||
* set the desired size. The colors, represented as R, G, B in 1-byte each were
|
||||
* converted to float using (Value - Mean)/Scale.
|
||||
*
|
||||
* imageHeight normalized image height.
|
||||
* imageWidth normalized image width.
|
||||
* mean mean value to normalize the input image.
|
||||
* scale scale to normalize the input image.
|
||||
* imageHeight normalized image height. imageWidth normalized image width. mean
|
||||
* mean value to normalize the input image. scale scale to normalize the input
|
||||
* image.
|
||||
*/
|
||||
this.imageNormalization = new GraphRunner("raw_image", "normalized_image")
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<String> input = tf.withName("raw_image").placeholder(String.class);
|
||||
final Operand<Float> decodedImage =
|
||||
tf.dtypes.cast(tf.image.decodeJpeg(input, DecodeJpeg.channels(3L)), Float.class);
|
||||
final Operand<Float> resizedImage = tf.image.resizeBilinear(
|
||||
tf.expandDims(decodedImage, tf.constant(0)),
|
||||
tf.constant(new int[] { imageHeight, imageWidth }));
|
||||
tf.withName("normalized_image").math.div(tf.math.sub(resizedImage, tf.constant(mean)), tf.constant(scale));
|
||||
});
|
||||
this.imageNormalization = new GraphRunner("raw_image", "normalized_image").withGraphDefinition(tf -> {
|
||||
Placeholder<String> input = tf.withName("raw_image").placeholder(String.class);
|
||||
final Operand<Float> decodedImage = tf.dtypes.cast(tf.image.decodeJpeg(input, DecodeJpeg.channels(3L)),
|
||||
Float.class);
|
||||
final Operand<Float> resizedImage = tf.image.resizeBilinear(tf.expandDims(decodedImage, tf.constant(0)),
|
||||
tf.constant(new int[] { imageHeight, imageWidth }));
|
||||
tf.withName("normalized_image").math.div(tf.math.sub(resizedImage, tf.constant(mean)), tf.constant(scale));
|
||||
});
|
||||
|
||||
this.imageRecognition = new GraphRunner(imageRecognitionGraphInputName, imageRecognitionGraphOutputName)
|
||||
.withGraphDefinition(new ProtoBufGraphDefinition(toResource(modelUri), cacheModel));
|
||||
.withGraphDefinition(new ProtoBufGraphDefinition(toResource(modelUri), cacheModel));
|
||||
|
||||
this.maxProbability = new GraphRunner(Arrays.asList("recognition_result"), Arrays.asList("category", "probability"))
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<Float> input = tf.withName("recognition_result").placeholder(Float.class);
|
||||
tf.withName("category").math.argMax(input, tf.constant(1));
|
||||
tf.withName("probability").max(input, tf.constant(1));
|
||||
});
|
||||
this.maxProbability = new GraphRunner(Arrays.asList("recognition_result"),
|
||||
Arrays.asList("category", "probability"))
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<Float> input = tf.withName("recognition_result").placeholder(Float.class);
|
||||
tf.withName("category").math.argMax(input, tf.constant(1));
|
||||
tf.withName("probability").max(input, tf.constant(1));
|
||||
});
|
||||
|
||||
this.topKProbabilities = new GraphRunner("recognition_result", "topK")
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<Float> input = tf.withName("recognition_result").placeholder(Float.class);
|
||||
tf.withName("topK").nn.topK(input, tf.constant(responseSize), TopK.sorted(true));
|
||||
});
|
||||
this.topKProbabilities = new GraphRunner("recognition_result", "topK").withGraphDefinition(tf -> {
|
||||
Placeholder<Float> input = tf.withName("recognition_result").placeholder(Float.class);
|
||||
tf.withName("topK").nn.topK(input, tf.constant(responseSize), TopK.sorted(true));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an byte encoded image and returns the most probable category recognized in the image along fromMemory its probability.
|
||||
* Takes an byte encoded image and returns the most probable category recognized in
|
||||
* the image along fromMemory its probability.
|
||||
* @param inputImage Byte array encoded image to recognize.
|
||||
* @return Returns a single map entry containing the names of the recognized categories as key and the confidence as value.
|
||||
* @return Returns a single map entry containing the names of the recognized
|
||||
* categories as key and the confidence as value.
|
||||
*/
|
||||
public Map<String, Double> recognizeMax(byte[] inputImage) {
|
||||
|
||||
try (Tensor inputTensor = Tensor.create(inputImage); GraphRunnerMemory memorize = new GraphRunnerMemory()) {
|
||||
|
||||
Map<String, Tensor<?>> max = this.imageNormalization.andThen(memorize)
|
||||
.andThen(this.imageRecognition).andThen(memorize)
|
||||
.andThen(this.maxProbability).andThen(memorize)
|
||||
.apply(Collections.singletonMap("raw_image", inputTensor));
|
||||
.andThen(this.imageRecognition)
|
||||
.andThen(memorize)
|
||||
.andThen(this.maxProbability)
|
||||
.andThen(memorize)
|
||||
.apply(Collections.singletonMap("raw_image", inputTensor));
|
||||
|
||||
long[] category = new long[1];
|
||||
max.get("category").copyTo(category);
|
||||
@@ -138,26 +148,28 @@ public class ImageRecognition implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an byte encoded input image and returns the top K most probable categories recognized in the image
|
||||
* along fromMemory their probabilities.
|
||||
*
|
||||
* Takes an byte encoded input image and returns the top K most probable categories
|
||||
* recognized in the image along fromMemory their probabilities.
|
||||
* @param inputImage Byte array encoded image to recognize.
|
||||
* @return Returns a list of key-value pairs. Every key-value pair represents a single category recognized.
|
||||
* The key stands for the name(s) of the category while the value states the confidence that there is an
|
||||
* object of this category. The entries in the Map are ordered from the higher to the lower confidences.
|
||||
* @return Returns a list of key-value pairs. Every key-value pair represents a single
|
||||
* category recognized. The key stands for the name(s) of the category while the value
|
||||
* states the confidence that there is an object of this category. The entries in the
|
||||
* Map are ordered from the higher to the lower confidences.
|
||||
*/
|
||||
public Map<String, Double> recognizeTopK(byte[] inputImage) {
|
||||
|
||||
try (Tensor inputTensor = Tensor.create(inputImage); GraphRunnerMemory memorize = new GraphRunnerMemory()) {
|
||||
|
||||
Map<String, Tensor<?>> topKResults =
|
||||
this.imageNormalization.andThen(memorize)
|
||||
.andThen(this.imageRecognition).andThen(memorize)
|
||||
.andThen(this.topKProbabilities).andThen(memorize)
|
||||
.apply(Collections.singletonMap("raw_image", inputTensor));
|
||||
Map<String, Tensor<?>> topKResults = this.imageNormalization.andThen(memorize)
|
||||
.andThen(this.imageRecognition)
|
||||
.andThen(memorize)
|
||||
.andThen(this.topKProbabilities)
|
||||
.andThen(memorize)
|
||||
.apply(Collections.singletonMap("raw_image", inputTensor));
|
||||
|
||||
Tensor recognizedImagesTensor = memorize.getTensorMap().get(this.imageRecognition.getSingleFetchName());
|
||||
float[][] results = new float[(int) recognizedImagesTensor.shape()[0]][(int) recognizedImagesTensor.shape()[1]];
|
||||
float[][] results = new float[(int) recognizedImagesTensor.shape()[0]][(int) recognizedImagesTensor
|
||||
.shape()[1]];
|
||||
recognizedImagesTensor.copyTo(results);
|
||||
|
||||
Tensor<Float> topKTensor = topKResults.get("topK").expect(Float.class);
|
||||
@@ -204,12 +216,10 @@ public class ImageRecognition implements AutoCloseable {
|
||||
* The Inception graph uses "input" as input and "output" as output.
|
||||
*
|
||||
*/
|
||||
public static ImageRecognition inception(String inceptionModelUri,
|
||||
int normalizedImageSize, int responseSize, boolean cacheModel) {
|
||||
return new ImageRecognition(inceptionModelUri, "classpath:/labels/inception_labels.txt",
|
||||
normalizedImageSize, normalizedImageSize, 117f, 1f,
|
||||
"input", "output",
|
||||
responseSize, cacheModel);
|
||||
public static ImageRecognition inception(String inceptionModelUri, int normalizedImageSize, int responseSize,
|
||||
boolean cacheModel) {
|
||||
return new ImageRecognition(inceptionModelUri, "classpath:/labels/inception_labels.txt", normalizedImageSize,
|
||||
normalizedImageSize, 117f, 1f, "input", "output", responseSize, cacheModel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,48 +228,50 @@ public class ImageRecognition implements AutoCloseable {
|
||||
*
|
||||
* The normalized image size is always square (e.g. H=W)
|
||||
*
|
||||
* The MobileNetV2 graph uses "input" as input and "MobilenetV2/Predictions/Reshape_1" as output.
|
||||
*
|
||||
* The MobileNetV2 graph uses "input" as input and "MobilenetV2/Predictions/Reshape_1"
|
||||
* as output.
|
||||
* @param mobileNetV2ModelUri model uri
|
||||
* @param normalizedImageSize Depends on the pre-trained model used. Usually 224px is used.
|
||||
* @param normalizedImageSize Depends on the pre-trained model used. Usually 224px is
|
||||
* used.
|
||||
* @param responseSize Number of responses fot topK requests.
|
||||
* @param cacheModel cache model
|
||||
* @return ImageRecognition instance configured fromMemory a MobileNetV2 pre-trained model.
|
||||
* @return ImageRecognition instance configured fromMemory a MobileNetV2 pre-trained
|
||||
* model.
|
||||
*/
|
||||
public static ImageRecognition mobileNetV2(String mobileNetV2ModelUri,
|
||||
int normalizedImageSize, int responseSize, boolean cacheModel) {
|
||||
return new ImageRecognition(mobileNetV2ModelUri, "classpath:/labels/mobilenet_labels.txt",
|
||||
normalizedImageSize, normalizedImageSize, 0f, 127f,
|
||||
"input", "MobilenetV2/Predictions/Reshape_1",
|
||||
responseSize, cacheModel);
|
||||
public static ImageRecognition mobileNetV2(String mobileNetV2ModelUri, int normalizedImageSize, int responseSize,
|
||||
boolean cacheModel) {
|
||||
return new ImageRecognition(mobileNetV2ModelUri, "classpath:/labels/mobilenet_labels.txt", normalizedImageSize,
|
||||
normalizedImageSize, 0f, 127f, "input", "MobilenetV2/Predictions/Reshape_1", responseSize, cacheModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience for MobileNetV1 pre-trained models:
|
||||
* https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet_v1.md#pre-trained-models
|
||||
*
|
||||
* The MobileNetV1 graph uses "input" as input and "MobilenetV1/Predictions/Reshape_1" as output.
|
||||
* The MobileNetV1 graph uses "input" as input and "MobilenetV1/Predictions/Reshape_1"
|
||||
* as output.
|
||||
*
|
||||
*/
|
||||
public static ImageRecognition mobileNetV1(String mobileNetV1ModelUri,
|
||||
int normalizedImageSize, int responseSize, boolean cacheModel) {
|
||||
return new ImageRecognition(mobileNetV1ModelUri, "classpath:/labels/mobilenet_labels.txt",
|
||||
normalizedImageSize, normalizedImageSize,
|
||||
0f, 127f,
|
||||
"input", "MobilenetV1/Predictions/Reshape_1",
|
||||
responseSize, cacheModel);
|
||||
public static ImageRecognition mobileNetV1(String mobileNetV1ModelUri, int normalizedImageSize, int responseSize,
|
||||
boolean cacheModel) {
|
||||
return new ImageRecognition(mobileNetV1ModelUri, "classpath:/labels/mobilenet_labels.txt", normalizedImageSize,
|
||||
normalizedImageSize, 0f, 127f, "input", "MobilenetV1/Predictions/Reshape_1", responseSize, cacheModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert image recognition results into {@link RecognitionResponse} domain list.
|
||||
* @param recognitionMap map containing the category mames and its probability. Returned by the
|
||||
* {@link ImageRecognition#recognizeMax(byte[])} and the ImageRecognition{@link #recognizeTopK(byte[])} methods
|
||||
* @return List of {@link RecognitionResponse} objects representing the name-to-probability pairs in the input map.
|
||||
* @param recognitionMap map containing the category mames and its probability.
|
||||
* Returned by the {@link ImageRecognition#recognizeMax(byte[])} and the
|
||||
* ImageRecognition{@link #recognizeTopK(byte[])} methods
|
||||
* @return List of {@link RecognitionResponse} objects representing the
|
||||
* name-to-probability pairs in the input map.
|
||||
*/
|
||||
public static List<RecognitionResponse> toRecognitionResponse(Map<String, Double> recognitionMap) {
|
||||
return recognitionMap.entrySet().stream()
|
||||
.map(nameProbabilityPair -> new RecognitionResponse(nameProbabilityPair.getKey(), nameProbabilityPair.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
return recognitionMap.entrySet()
|
||||
.stream()
|
||||
.map(nameProbabilityPair -> new RecognitionResponse(nameProbabilityPair.getKey(),
|
||||
nameProbabilityPair.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -269,4 +281,5 @@ public class ImageRecognition implements AutoCloseable {
|
||||
this.maxProbability.close();
|
||||
this.topKProbabilities.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import javax.imageio.ImageIO;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Ability to to augment the input image fromMemory the recognized labels.
|
||||
*
|
||||
@@ -47,6 +46,7 @@ public class ImageRecognitionAugmenter implements BiFunction<byte[], List<Recogn
|
||||
public static final String IMAGE_FORMAT = "jpg";
|
||||
|
||||
private final Color textColor = Color.BLACK;
|
||||
|
||||
private final Color bgColor = new Color(167, 252, 0);
|
||||
|
||||
public ImageRecognitionAugmenter() {
|
||||
@@ -54,7 +54,6 @@ public class ImageRecognitionAugmenter implements BiFunction<byte[], List<Recogn
|
||||
|
||||
/**
|
||||
* Augment the input image by adding the recognized classes.
|
||||
*
|
||||
* @param imageBytes input image as byte array
|
||||
* @param result computed recognition labels
|
||||
* @return the image augmented fromMemory recognized labels.
|
||||
|
||||
@@ -20,7 +20,9 @@ package org.springframework.cloud.fn.image.recognition;
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class RecognitionResponse {
|
||||
|
||||
private String label;
|
||||
|
||||
private Double probability;
|
||||
|
||||
public RecognitionResponse() {
|
||||
@@ -51,4 +53,5 @@ public class RecognitionResponse {
|
||||
public String toString() {
|
||||
return "{label='" + label + ", probability=" + probability + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,22 +36,20 @@ import org.springframework.util.StreamUtils;
|
||||
/**
|
||||
* Create a text file mapping label id to human readable string.
|
||||
*
|
||||
* Produces a text file where every line represents single category. The line number represents the category id, while
|
||||
* the line text is human-readable names for the categories fromMemory this imagenet id.
|
||||
* Produces a text file where every line represents single category. The line number
|
||||
* represents the category id, while the line text is human-readable names for the
|
||||
* categories fromMemory this imagenet id.
|
||||
*
|
||||
* Based on https://github.com/tensorflow/models/blob/master/research/slim/datasets/imagenet.py#L66
|
||||
* Based on
|
||||
* https://github.com/tensorflow/models/blob/master/research/slim/datasets/imagenet.py#L66
|
||||
*
|
||||
* We retrieve a synset file, which contains a list of valid synset labels used
|
||||
* by ILSVRC competition. There is one synset one per line, eg.
|
||||
* # n01440764
|
||||
* # n01443537
|
||||
* We also retrieve a synset_to_human_file, which contains a mapping from synsets
|
||||
* to human-readable names for every synset in Imagenet. These are stored in a
|
||||
* tsv format, as follows:
|
||||
* # n02119247 black fox
|
||||
* # n02119359 silver fox
|
||||
* We assign each synset (in alphabetical order) an integer, starting from 1
|
||||
* (since 0 is reserved for the background class)
|
||||
* We retrieve a synset file, which contains a list of valid synset labels used by ILSVRC
|
||||
* competition. There is one synset one per line, eg. # n01440764 # n01443537 We also
|
||||
* retrieve a synset_to_human_file, which contains a mapping from synsets to
|
||||
* human-readable names for every synset in Imagenet. These are stored in a tsv format, as
|
||||
* follows: # n02119247 black fox # n02119359 silver fox We assign each synset (in
|
||||
* alphabetical order) an integer, starting from 1 (since 0 is reserved for the background
|
||||
* class)
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -62,8 +60,10 @@ public final class ImageNetReadableNamesWriter {
|
||||
|
||||
/** BASE_URL. */
|
||||
public final static String BASE_URL = "https://raw.githubusercontent.com/tensorflow/models/master/research/inception/inception/data/";
|
||||
|
||||
/** SYNSET_URI. */
|
||||
public final static String SYNSET_URI = BASE_URL + "imagenet_lsvrc_2015_synsets.txt";
|
||||
|
||||
/** SYNSET_TO_HUMAN_URI. */
|
||||
public final static String SYNSET_TO_HUMAN_URI = BASE_URL + "imagenet_metadata.txt";
|
||||
|
||||
@@ -71,19 +71,24 @@ public final class ImageNetReadableNamesWriter {
|
||||
Charset utf8 = Charset.forName("UTF-8");
|
||||
|
||||
try (InputStream synsetIs = toResource(SYNSET_URI).getInputStream();
|
||||
InputStream synsetToHumanIs = toResource(SYNSET_TO_HUMAN_URI).getInputStream()) {
|
||||
InputStream synsetToHumanIs = toResource(SYNSET_TO_HUMAN_URI).getInputStream()) {
|
||||
|
||||
List<String> synsetList = Arrays.asList(StreamUtils.copyToString(synsetIs, utf8)
|
||||
.split("\n")).stream().map(l -> l.trim()).collect(Collectors.toList());
|
||||
List<String> synsetList = Arrays.asList(StreamUtils.copyToString(synsetIs, utf8).split("\n"))
|
||||
.stream()
|
||||
.map(l -> l.trim())
|
||||
.collect(Collectors.toList());
|
||||
Assert.notNull(synsetList, "Failed to initialize the labels list");
|
||||
Assert.isTrue(synsetList.size() == 1000, "Labels list is expected to be of " +
|
||||
"size 1000 but was:" + synsetList.size());
|
||||
Assert.isTrue(synsetList.size() == 1000,
|
||||
"Labels list is expected to be of " + "size 1000 but was:" + synsetList.size());
|
||||
|
||||
Map<String, String> synsetToHuman = Arrays.asList(StreamUtils.copyToString(synsetToHumanIs, utf8)
|
||||
.split("\n")).stream().map(s2h -> s2h.split("\t")).collect(Collectors.toMap(s -> s[0], s -> s[1]));
|
||||
Map<String, String> synsetToHuman = Arrays
|
||||
.asList(StreamUtils.copyToString(synsetToHumanIs, utf8).split("\n"))
|
||||
.stream()
|
||||
.map(s2h -> s2h.split("\t"))
|
||||
.collect(Collectors.toMap(s -> s[0], s -> s[1]));
|
||||
Assert.notNull(synsetToHuman, "Failed to initialize the synsetToHuman");
|
||||
Assert.isTrue(synsetToHuman.size() == 21842, "synsetToHuman is expected to be of " +
|
||||
"size 21842 but was:" + synsetToHuman.size());
|
||||
Assert.isTrue(synsetToHuman.size() == 21842,
|
||||
"synsetToHuman is expected to be of " + "size 21842 but was:" + synsetToHuman.size());
|
||||
|
||||
List<String> l = synsetList.stream().map(id -> synsetToHuman.get(id)).collect(Collectors.toList());
|
||||
|
||||
@@ -102,4 +107,5 @@ public final class ImageNetReadableNamesWriter {
|
||||
public static Resource toResource(String uri) {
|
||||
return new DefaultResourceLoader().getResource(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,62 +42,51 @@ public final class ImageRecognitionExample {
|
||||
// MmobileNetV2 models
|
||||
// https://github.com/tensorflow/models/tree/master/research/slim/nets/mobilenet#pretrained-models
|
||||
String mobilenet_v2_modelUri = "https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_1.4_224.tgz#mobilenet_v2_1.4_224_frozen.pb";
|
||||
//String mobilenet_v2_modelUri = "https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_96.tgz#mobilenet_v2_0.35_96_frozen.pb";
|
||||
try (ImageRecognition imageRecognition = ImageRecognition.mobileNetV2(
|
||||
mobilenet_v2_modelUri,
|
||||
224,
|
||||
5,
|
||||
true)) {
|
||||
// String mobilenet_v2_modelUri =
|
||||
// "https://storage.googleapis.com/mobilenet_v2/checkpoints/mobilenet_v2_0.35_96.tgz#mobilenet_v2_0.35_96_frozen.pb";
|
||||
try (ImageRecognition imageRecognition = ImageRecognition.mobileNetV2(mobilenet_v2_modelUri, 224, 5, true)) {
|
||||
|
||||
List<RecognitionResponse> recognizedObjects =
|
||||
ImageRecognition.toRecognitionResponse(imageRecognition.recognizeTopK(inputImage));
|
||||
List<RecognitionResponse> recognizedObjects = ImageRecognition
|
||||
.toRecognitionResponse(imageRecognition.recognizeTopK(inputImage));
|
||||
|
||||
// Draw the predicted labels on top of the input image.
|
||||
byte[] augmentedImage = new ImageRecognitionAugmenter().apply(inputImage, recognizedObjects);
|
||||
IOUtils.write(augmentedImage, new FileOutputStream("./image-recognition/target/image-augmented-mobilnetV2.jpg"));
|
||||
|
||||
IOUtils.write(augmentedImage,
|
||||
new FileOutputStream("./image-recognition/target/image-augmented-mobilnetV2.jpg"));
|
||||
|
||||
String jsonRecognizedObjects = new JsonMapperFunction().apply(recognizedObjects);
|
||||
System.out.println("mobilnetV2 result:" + jsonRecognizedObjects);
|
||||
}
|
||||
|
||||
|
||||
String mobilenet_v1_modelUri = "https://download.tensorflow.org/models/mobilenet_v1_2018_08_02/mobilenet_v1_1.0_224.tgz#mobilenet_v1_1.0_224_frozen.pb";
|
||||
try (ImageRecognition recognitionService = ImageRecognition.mobileNetV1(
|
||||
mobilenet_v1_modelUri,
|
||||
224,
|
||||
5,
|
||||
true)) {
|
||||
try (ImageRecognition recognitionService = ImageRecognition.mobileNetV1(mobilenet_v1_modelUri, 224, 5, true)) {
|
||||
|
||||
List<RecognitionResponse> recognizedObjects =
|
||||
ImageRecognition.toRecognitionResponse(recognitionService.recognizeTopK(inputImage));
|
||||
List<RecognitionResponse> recognizedObjects = ImageRecognition
|
||||
.toRecognitionResponse(recognitionService.recognizeTopK(inputImage));
|
||||
|
||||
// Draw the predicted labels on top of the input image.
|
||||
byte[] augmentedImage = new ImageRecognitionAugmenter().apply(inputImage, recognizedObjects);
|
||||
IOUtils.write(augmentedImage, new FileOutputStream("./image-recognition/target/image-augmented-mobilnetV1.jpg"));
|
||||
|
||||
IOUtils.write(augmentedImage,
|
||||
new FileOutputStream("./image-recognition/target/image-augmented-mobilnetV1.jpg"));
|
||||
|
||||
String jsonRecognizedObjects = new JsonMapperFunction().apply(recognizedObjects);
|
||||
System.out.println("mobilnetV1 result:" + jsonRecognizedObjects);
|
||||
}
|
||||
|
||||
String inception_modelUri = "https://storage.googleapis.com/scdf-tensorflow-models/image-recognition/tensorflow_inception_graph.pb";
|
||||
try (ImageRecognition recognitionService = ImageRecognition.inception(
|
||||
inception_modelUri,
|
||||
224,
|
||||
5,
|
||||
true)) {
|
||||
try (ImageRecognition recognitionService = ImageRecognition.inception(inception_modelUri, 224, 5, true)) {
|
||||
|
||||
List<RecognitionResponse> recognizedObjects =
|
||||
ImageRecognition.toRecognitionResponse(recognitionService.recognizeTopK(inputImage));
|
||||
List<RecognitionResponse> recognizedObjects = ImageRecognition
|
||||
.toRecognitionResponse(recognitionService.recognizeTopK(inputImage));
|
||||
|
||||
// Draw the predicted labels on top of the input image.
|
||||
byte[] augmentedImage = new ImageRecognitionAugmenter().apply(inputImage, recognizedObjects);
|
||||
IOUtils.write(augmentedImage, new FileOutputStream("./image-recognition/target/image-augmented-inception.jpg"));
|
||||
|
||||
IOUtils.write(augmentedImage,
|
||||
new FileOutputStream("./image-recognition/target/image-augmented-inception.jpg"));
|
||||
|
||||
String jsonRecognizedObjects = new JsonMapperFunction().apply(recognizedObjects);
|
||||
System.out.println("inception result:" + jsonRecognizedObjects);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,8 +45,11 @@ public final class ImageRecognitionExample2 {
|
||||
System.out.println(inceptions.recognizeTopK(inputImage));
|
||||
System.out.println(ImageRecognition.toRecognitionResponse(inceptions.recognizeTopK(inputImage)));
|
||||
|
||||
IOUtils.write(augmenter.apply(inputImage, ImageRecognition.toRecognitionResponse(inceptions.recognizeTopK(inputImage))),
|
||||
new FileOutputStream("./functions/function/image-recognition-function/target/image-augmented-inceptions.jpg"));
|
||||
IOUtils.write(
|
||||
augmenter.apply(inputImage,
|
||||
ImageRecognition.toRecognitionResponse(inceptions.recognizeTopK(inputImage))),
|
||||
new FileOutputStream(
|
||||
"./functions/function/image-recognition-function/target/image-augmented-inceptions.jpg"));
|
||||
inceptions.close();
|
||||
|
||||
ImageRecognition mobileNetV2 = ImageRecognition.mobileNetV2(
|
||||
@@ -54,8 +57,11 @@ public final class ImageRecognitionExample2 {
|
||||
224, 10, true);
|
||||
System.out.println(mobileNetV2.recognizeMax(inputImage));
|
||||
System.out.println(mobileNetV2.recognizeTopK(inputImage));
|
||||
IOUtils.write(augmenter.apply(inputImage, ImageRecognition.toRecognitionResponse(mobileNetV2.recognizeTopK(inputImage))),
|
||||
new FileOutputStream("./functions/function/image-recognition-function/target/image-augmented-mobilnetV2.jpg"));
|
||||
IOUtils.write(
|
||||
augmenter.apply(inputImage,
|
||||
ImageRecognition.toRecognitionResponse(mobileNetV2.recognizeTopK(inputImage))),
|
||||
new FileOutputStream(
|
||||
"./functions/function/image-recognition-function/target/image-augmented-mobilnetV2.jpg"));
|
||||
mobileNetV2.close();
|
||||
|
||||
ImageRecognition mobileNetV1 = ImageRecognition.mobileNetV1(
|
||||
@@ -63,8 +69,12 @@ public final class ImageRecognitionExample2 {
|
||||
224, 10, true);
|
||||
System.out.println(mobileNetV1.recognizeMax(inputImage));
|
||||
System.out.println(mobileNetV1.recognizeTopK(inputImage));
|
||||
IOUtils.write(augmenter.apply(inputImage, ImageRecognition.toRecognitionResponse(mobileNetV1.recognizeTopK(inputImage))),
|
||||
new FileOutputStream("./functions/function/image-recognition-function/target/image-augmented-mobilnetV1.jpg"));
|
||||
IOUtils.write(
|
||||
augmenter.apply(inputImage,
|
||||
ImageRecognition.toRecognitionResponse(mobileNetV1.recognizeTopK(inputImage))),
|
||||
new FileOutputStream(
|
||||
"./functions/function/image-recognition-function/target/image-augmented-mobilnetV1.jpg"));
|
||||
mobileNetV1.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.springframework.cloud.fn.image.recognition;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.tensorflow.SavedModelBundle;
|
||||
import org.tensorflow.framework.MetaGraphDef;
|
||||
@@ -40,11 +39,13 @@ public final class SavedModelTest {
|
||||
*
|
||||
*/
|
||||
public static void main(String[] args) throws InvalidProtocolBufferException {
|
||||
SavedModelBundle savedModelBundle =
|
||||
SavedModelBundle.load("/Users/ctzolov/Downloads/ssd_mobilenet_v1_coco_2017_11_17/saved_model", "serve");
|
||||
//SavedModelBundle.load("/Users/ctzolov/Downloads/aiy_vision_classifier_plants_V1_1/", "serve");
|
||||
//SavedModelBundle savedModelBundle =
|
||||
// SavedModelBundle.load("/Users/ctzolov/Downloads/mnasnet-a1/saved_model", "serve");
|
||||
SavedModelBundle savedModelBundle = SavedModelBundle
|
||||
.load("/Users/ctzolov/Downloads/ssd_mobilenet_v1_coco_2017_11_17/saved_model", "serve");
|
||||
// SavedModelBundle.load("/Users/ctzolov/Downloads/aiy_vision_classifier_plants_V1_1/",
|
||||
// "serve");
|
||||
// SavedModelBundle savedModelBundle =
|
||||
// SavedModelBundle.load("/Users/ctzolov/Downloads/mnasnet-a1/saved_model",
|
||||
// "serve");
|
||||
|
||||
MetaGraphDef meta = MetaGraphDef.parseFrom(savedModelBundle.metaGraphDef());
|
||||
|
||||
@@ -54,10 +55,11 @@ public final class SavedModelTest {
|
||||
|
||||
savedModelBundle.session();
|
||||
|
||||
//Iterator<Operation> itr = savedModelBundle.graph().operations();
|
||||
// Iterator<Operation> itr = savedModelBundle.graph().operations();
|
||||
//
|
||||
//while (itr.hasNext()) {
|
||||
// System.out.println("Operation: " + itr.next());
|
||||
//}
|
||||
// while (itr.hasNext()) {
|
||||
// System.out.println("Operation: " + itr.next());
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ import org.springframework.cloud.fn.object.detection.domain.ObjectDetection;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Augment the input image fromMemory detected object bounding boxes and categories.
|
||||
* For mask models and withMask set to true it draws the instance segmentation image as well.
|
||||
* Augment the input image fromMemory detected object bounding boxes and categories. For
|
||||
* mask models and withMask set to true it draws the instance segmentation image as well.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -48,6 +48,7 @@ public class ObjectDetectionImageAugmenter implements BiFunction<byte[], List<Ob
|
||||
private String imageFormat = DEFAULT_IMAGE_FORMAT;
|
||||
|
||||
private final boolean withMask;
|
||||
|
||||
private boolean agnosticColors = false;
|
||||
|
||||
public ObjectDetectionImageAugmenter() {
|
||||
@@ -99,8 +100,7 @@ public class ObjectDetectionImageAugmenter implements BiFunction<byte[], List<Ob
|
||||
float[][] mask = od.getMask();
|
||||
if (mask != null) {
|
||||
Color maskColor = this.agnosticColors ? null : GraphicsUtils.getClassColor(cid);
|
||||
BufferedImage maskImage = GraphicsUtils.createMaskImage(
|
||||
mask, x2 - x1, y2 - y1, maskColor);
|
||||
BufferedImage maskImage = GraphicsUtils.createMaskImage(mask, x2 - x1, y2 - y1, maskColor);
|
||||
GraphicsUtils.overlayImages(bufferedImage, maskImage, x1, y1);
|
||||
}
|
||||
}
|
||||
@@ -116,4 +116,5 @@ public class ObjectDetectionImageAugmenter implements BiFunction<byte[], List<Ob
|
||||
// Null mend that QR image is found and not output message will be send.
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,8 +41,10 @@ public class ObjectDetectionInputAdapter implements Function<byte[], Map<String,
|
||||
|
||||
/** Make checkstyle happy. **/
|
||||
public static final String RAW_IMAGE = "raw_image";
|
||||
|
||||
/** Make checkstyle happy. **/
|
||||
public static final String NORMALIZED_IMAGE = "normalized_image";
|
||||
|
||||
/** Make checkstyle happy. **/
|
||||
public static final long CHANNELS = 3;
|
||||
|
||||
@@ -50,14 +52,14 @@ public class ObjectDetectionInputAdapter implements Function<byte[], Map<String,
|
||||
|
||||
public ObjectDetectionInputAdapter() {
|
||||
|
||||
this.imageLoaderGraph = new GraphRunner(RAW_IMAGE, NORMALIZED_IMAGE)
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<String> rawImage = tf.withName(RAW_IMAGE).placeholder(String.class);
|
||||
Operand<UInt8> decodedImage = tf.dtypes.cast(
|
||||
tf.image.decodeJpeg(rawImage, DecodeJpeg.channels(CHANNELS)), UInt8.class);
|
||||
// Expand dimensions since the model expects images to have shape: [1, H, W, 3]
|
||||
tf.withName(NORMALIZED_IMAGE).expandDims(decodedImage, tf.constant(0));
|
||||
});
|
||||
this.imageLoaderGraph = new GraphRunner(RAW_IMAGE, NORMALIZED_IMAGE).withGraphDefinition(tf -> {
|
||||
Placeholder<String> rawImage = tf.withName(RAW_IMAGE).placeholder(String.class);
|
||||
Operand<UInt8> decodedImage = tf.dtypes.cast(tf.image.decodeJpeg(rawImage, DecodeJpeg.channels(CHANNELS)),
|
||||
UInt8.class);
|
||||
// Expand dimensions since the model expects images to have shape: [1, H, W,
|
||||
// 3]
|
||||
tf.withName(NORMALIZED_IMAGE).expandDims(decodedImage, tf.constant(0));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,4 +75,5 @@ public class ObjectDetectionInputAdapter implements Function<byte[], Map<String,
|
||||
this.imageLoaderGraph.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ import org.tensorflow.types.UInt8;
|
||||
import org.springframework.cloud.fn.common.tensorflow.deprecated.GraphicsUtils;
|
||||
|
||||
/**
|
||||
* Converts byte array image into a input Tensor for the Object Detection API. The computed image tensors uses the
|
||||
* 'image_tensor' model placeholder.
|
||||
* Converts byte array image into a input Tensor for the Object Detection API. The
|
||||
* computed image tensors uses the 'image_tensor' model placeholder.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -95,4 +95,5 @@ public class ObjectDetectionInputConverter implements Function<byte[][], Map<Str
|
||||
data[i + 2] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,21 +37,22 @@ import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Converts the Tensorflow Object Detection result into {@link ObjectDetection} list.
|
||||
* The pre-trained Object Detection models (http://bit.ly/2osxMAY) produce 3 tensor outputs:
|
||||
* (1) detection_classes - containing the ids of detected objects, (2) detection_scores - confidence probabilities of the
|
||||
* detected object and (3) detection_boxes - the object bounding boxes withing the images.
|
||||
* Converts the Tensorflow Object Detection result into {@link ObjectDetection} list. The
|
||||
* pre-trained Object Detection models (http://bit.ly/2osxMAY) produce 3 tensor outputs:
|
||||
* (1) detection_classes - containing the ids of detected objects, (2) detection_scores -
|
||||
* confidence probabilities of the detected object and (3) detection_boxes - the object
|
||||
* bounding boxes withing the images.
|
||||
*
|
||||
* The MASK based models provide to 2 additional tensors: (4) num_detections and (5) detection_masks.
|
||||
* The MASK based models provide to 2 additional tensors: (4) num_detections and (5)
|
||||
* detection_masks.
|
||||
*
|
||||
* All outputs tensors are float arrays, having:
|
||||
* - 1 as the first dimension
|
||||
* - maxObjects as the second dimension
|
||||
* While boxesT will have 4 as the third dimension (2 sets of (x, y) coordinates).
|
||||
* This can be verified by looking at scoresT.shape() etc.
|
||||
* All outputs tensors are float arrays, having: - 1 as the first dimension - maxObjects
|
||||
* as the second dimension While boxesT will have 4 as the third dimension (2 sets of (x,
|
||||
* y) coordinates). This can be verified by looking at scoresT.shape() etc.
|
||||
*
|
||||
* The format detected classes (e.g. labels) names is defined by the 'string_int_labels_map.proto'. The input list
|
||||
* is available at: https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
* The format detected classes (e.g. labels) names is defined by the
|
||||
* 'string_int_labels_map.proto'. The input list is available at:
|
||||
* https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -61,17 +62,23 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
|
||||
/** DETECTION_CLASSES. */
|
||||
public static final String DETECTION_CLASSES = "detection_classes";
|
||||
|
||||
/** DETECTION_SCORES. */
|
||||
public static final String DETECTION_SCORES = "detection_scores";
|
||||
|
||||
/** DETECTION_BOXES. */
|
||||
public static final String DETECTION_BOXES = "detection_boxes";
|
||||
|
||||
/** DETECTION_MASKS. */
|
||||
public static final String DETECTION_MASKS = "detection_masks";
|
||||
|
||||
/** NUM_DETECTIONS. */
|
||||
public static final String NUM_DETECTIONS = "num_detections";
|
||||
|
||||
private final String[] labels;
|
||||
|
||||
private float confidence;
|
||||
|
||||
private List<String> modelFetch;
|
||||
|
||||
public ObjectDetectionOutputConverter(Resource labelsResource, float confidence, List<String> modelFetch) {
|
||||
@@ -96,15 +103,16 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
private static String[] loadLabels(Resource labelsResource) throws Exception {
|
||||
try (InputStream is = labelsResource.getInputStream()) {
|
||||
String text = StreamUtils.copyToString(is, Charset.forName("UTF-8"));
|
||||
StringIntLabelMapOuterClass.StringIntLabelMap.Builder builder =
|
||||
StringIntLabelMapOuterClass.StringIntLabelMap.newBuilder();
|
||||
StringIntLabelMapOuterClass.StringIntLabelMap.Builder builder = StringIntLabelMapOuterClass.StringIntLabelMap
|
||||
.newBuilder();
|
||||
TextFormat.merge(text, builder);
|
||||
StringIntLabelMapOuterClass.StringIntLabelMap proto = builder.build();
|
||||
|
||||
int maxLabelId = proto.getItemList().stream()
|
||||
.map(StringIntLabelMapOuterClass.StringIntLabelMapItem::getId)
|
||||
.max(Comparator.comparing(i -> i))
|
||||
.orElse(-1);
|
||||
int maxLabelId = proto.getItemList()
|
||||
.stream()
|
||||
.map(StringIntLabelMapOuterClass.StringIntLabelMapItem::getId)
|
||||
.max(Comparator.comparing(i -> i))
|
||||
.orElse(-1);
|
||||
|
||||
String[] labelIdToNameMap = new String[maxLabelId + 1];
|
||||
for (StringIntLabelMapOuterClass.StringIntLabelMapItem item : proto.getItemList()) {
|
||||
@@ -112,7 +120,8 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
labelIdToNameMap[item.getId()] = item.getDisplayName();
|
||||
}
|
||||
else {
|
||||
// Common practice is to set the name to a MID or Synsets Id. Synset is a set of synonyms that
|
||||
// Common practice is to set the name to a MID or Synsets Id. Synset
|
||||
// is a set of synonyms that
|
||||
// share a common meaning: https://en.wikipedia.org/wiki/WordNet
|
||||
labelIdToNameMap[item.getId()] = item.getName();
|
||||
}
|
||||
@@ -125,13 +134,13 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
public List<List<ObjectDetection>> apply(Map<String, Tensor<?>> tensorMap) {
|
||||
|
||||
try (Tensor<Float> scoresTensor = tensorMap.get(DETECTION_SCORES).expect(Float.class);
|
||||
Tensor<Float> classesTensor = tensorMap.get(DETECTION_CLASSES).expect(Float.class);
|
||||
Tensor<Float> boxesTensor = tensorMap.get(DETECTION_BOXES).expect(Float.class)
|
||||
) {
|
||||
Tensor<Float> classesTensor = tensorMap.get(DETECTION_CLASSES).expect(Float.class);
|
||||
Tensor<Float> boxesTensor = tensorMap.get(DETECTION_BOXES).expect(Float.class)) {
|
||||
// All these tensors have:
|
||||
// - 1 as the first dimension
|
||||
// - maxObjects as the second dimension
|
||||
// While boxesT will have 4 as the third dimension (2 sets of (x, y) coordinates).
|
||||
// While boxesT will have 4 as the third dimension (2 sets of (x, y)
|
||||
// coordinates).
|
||||
// This can be verified by looking at scoresT.shape() etc.
|
||||
int batchSize = (int) scoresTensor.shape()[0];
|
||||
int maxObjects = (int) scoresTensor.shape()[1];
|
||||
@@ -143,10 +152,10 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
|
||||
for (int batchIndex = 0; batchIndex < batchSize; batchIndex++) {
|
||||
|
||||
|
||||
List<ObjectDetection> objectDetections = new ArrayList<>();
|
||||
|
||||
// Collect only the objects whose scores are at above the configured confidence threshold.
|
||||
// Collect only the objects whose scores are at above the configured
|
||||
// confidence threshold.
|
||||
for (int i = 0; i < scores[batchIndex].length; ++i) {
|
||||
if (scores[batchIndex][i] >= confidence) {
|
||||
String category = labels[(int) classes[batchIndex][i]];
|
||||
@@ -169,7 +178,8 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
|
||||
if (masksTensor != null) {
|
||||
long[] shape = masksTensor.shape();
|
||||
float[][][][] masks = masksTensor.copyTo(new float[(int) shape[0]][(int) shape[1]][(int) shape[2]][(int) shape[3]]);
|
||||
float[][][][] masks = masksTensor
|
||||
.copyTo(new float[(int) shape[0]][(int) shape[1]][(int) shape[2]][(int) shape[3]]);
|
||||
od.setMask(masks[batchIndex][i]);
|
||||
logger.debug(String.format("Num detections: %s, Masks: %s", nd, masks));
|
||||
}
|
||||
@@ -184,4 +194,5 @@ public class ObjectDetectionOutputConverter implements Function<Map<String, Tens
|
||||
return batchObjectDetections;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,66 +29,79 @@ import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* Convenience class that leverages the the {@link ObjectDetectionInputConverter}, {@link ObjectDetectionOutputConverter} and {@link TensorFlowService}
|
||||
* in combination fromMemory the Tensorflow Object Detection API (https://github.com/tensorflow/models/tree/master/research/object_detection)
|
||||
* models for detection objects in input images.
|
||||
* Convenience class that leverages the the {@link ObjectDetectionInputConverter},
|
||||
* {@link ObjectDetectionOutputConverter} and {@link TensorFlowService} in combination
|
||||
* fromMemory the Tensorflow Object Detection API
|
||||
* (https://github.com/tensorflow/models/tree/master/research/object_detection) models for
|
||||
* detection objects in input images.
|
||||
*
|
||||
* All pre-trained models (https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md) and labels are supported.
|
||||
* All pre-trained models
|
||||
* (https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md)
|
||||
* and labels are supported.
|
||||
*
|
||||
* You can download pre-trained models directly from the zoo: https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
* Just use the URI notation: (zoo model tar.gz url)#(name of the frozen model file name). To speedup the bootstrap
|
||||
* performance you should consider downloading the models locally and use the file:/"path to my model" URI instead!
|
||||
* You can download pre-trained models directly from the zoo:
|
||||
* https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
* Just use the URI notation: (zoo model tar.gz url)#(name of the frozen model file name).
|
||||
* To speedup the bootstrap performance you should consider downloading the models locally
|
||||
* and use the file:/"path to my model" URI instead!
|
||||
*
|
||||
* The object category labels for the pre-trained models are available at: https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
* Use the labels applicable for the model. Also, for performance reasons you may consider to download the labels
|
||||
* and load them from file: instead.
|
||||
* The object category labels for the pre-trained models are available at:
|
||||
* https://github.com/tensorflow/models/tree/master/research/object_detection/data Use the
|
||||
* labels applicable for the model. Also, for performance reasons you may consider to
|
||||
* download the labels and load them from file: instead.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class ObjectDetectionService {
|
||||
|
||||
/** Default list of fetch names for Box models. */
|
||||
public static List<String> FETCH_NAMES = Arrays.asList(
|
||||
ObjectDetectionOutputConverter.DETECTION_SCORES, ObjectDetectionOutputConverter.DETECTION_CLASSES,
|
||||
ObjectDetectionOutputConverter.DETECTION_BOXES, ObjectDetectionOutputConverter.NUM_DETECTIONS);
|
||||
|
||||
/** Default list of fetch names for mask supporting models. */
|
||||
public static List<String> FETCH_NAMES_WITH_MASKS = Arrays.asList(
|
||||
ObjectDetectionOutputConverter.DETECTION_SCORES, ObjectDetectionOutputConverter.DETECTION_CLASSES,
|
||||
ObjectDetectionOutputConverter.DETECTION_BOXES, ObjectDetectionOutputConverter.DETECTION_MASKS,
|
||||
public static List<String> FETCH_NAMES = Arrays.asList(ObjectDetectionOutputConverter.DETECTION_SCORES,
|
||||
ObjectDetectionOutputConverter.DETECTION_CLASSES, ObjectDetectionOutputConverter.DETECTION_BOXES,
|
||||
ObjectDetectionOutputConverter.NUM_DETECTIONS);
|
||||
|
||||
/** Default list of fetch names for mask supporting models. */
|
||||
public static List<String> FETCH_NAMES_WITH_MASKS = Arrays.asList(ObjectDetectionOutputConverter.DETECTION_SCORES,
|
||||
ObjectDetectionOutputConverter.DETECTION_CLASSES, ObjectDetectionOutputConverter.DETECTION_BOXES,
|
||||
ObjectDetectionOutputConverter.DETECTION_MASKS, ObjectDetectionOutputConverter.NUM_DETECTIONS);
|
||||
|
||||
private final ObjectDetectionInputConverter inputConverter;
|
||||
|
||||
private final ObjectDetectionOutputConverter outputConverter;
|
||||
|
||||
private final TensorFlowService tensorFlowService;
|
||||
|
||||
public ObjectDetectionService() {
|
||||
this("https://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz#frozen_inference_graph.pb",
|
||||
"https://storage.googleapis.com/scdf-tensorflow-models/object-detection/mscoco_label_map.pbtxt",
|
||||
0.4f, false, true);
|
||||
"https://storage.googleapis.com/scdf-tensorflow-models/object-detection/mscoco_label_map.pbtxt", 0.4f,
|
||||
false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor that would initialize all necessary internal components.
|
||||
* @param modelUri URI of the pre-trained, frozen Tensorflow model.
|
||||
* @param labelsUri URI of the pre-trained category labels.
|
||||
* @param confidence Confidence threshold. Only objects detected wth confidence above this threshold will be returned.
|
||||
* @param withMasks If a Mask model is selected then you can use this flag to extract the instance segmentation masks as well.
|
||||
* @param confidence Confidence threshold. Only objects detected wth confidence above
|
||||
* this threshold will be returned.
|
||||
* @param withMasks If a Mask model is selected then you can use this flag to extract
|
||||
* the instance segmentation masks as well.
|
||||
*/
|
||||
public ObjectDetectionService(String modelUri, String labelsUri,
|
||||
float confidence, boolean withMasks, boolean cacheModel) {
|
||||
public ObjectDetectionService(String modelUri, String labelsUri, float confidence, boolean withMasks,
|
||||
boolean cacheModel) {
|
||||
this.inputConverter = new ObjectDetectionInputConverter();
|
||||
List<String> fetchNames = withMasks ? FETCH_NAMES_WITH_MASKS : FETCH_NAMES;
|
||||
this.outputConverter = new ObjectDetectionOutputConverter(
|
||||
new DefaultResourceLoader().getResource(labelsUri), confidence, fetchNames);
|
||||
this.tensorFlowService = new TensorFlowService(
|
||||
new DefaultResourceLoader().getResource(modelUri), fetchNames, cacheModel);
|
||||
this.outputConverter = new ObjectDetectionOutputConverter(new DefaultResourceLoader().getResource(labelsUri),
|
||||
confidence, fetchNames);
|
||||
this.tensorFlowService = new TensorFlowService(new DefaultResourceLoader().getResource(modelUri), fetchNames,
|
||||
cacheModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic constructor thea allow the converter to be pre-configured before passed to the service.
|
||||
* @param inputConverter Converter from byte array to object detection input image tensor
|
||||
* @param outputConverter Covets the object detection output tensors into {@link ObjectDetection } list
|
||||
* Generic constructor thea allow the converter to be pre-configured before passed to
|
||||
* the service.
|
||||
* @param inputConverter Converter from byte array to object detection input image
|
||||
* tensor
|
||||
* @param outputConverter Covets the object detection output tensors into
|
||||
* {@link ObjectDetection } list
|
||||
* @param tensorFlowService Java tensorflow runner instance
|
||||
*/
|
||||
public ObjectDetectionService(ObjectDetectionInputConverter inputConverter,
|
||||
@@ -100,9 +113,9 @@ public class ObjectDetectionService {
|
||||
|
||||
/**
|
||||
* Detects objects in a single input image identified by its URI.
|
||||
*
|
||||
* @param imageUri input image's URI
|
||||
* @return Returns a list of {@link ObjectDetection} domain objects representing detected objects
|
||||
* @return Returns a list of {@link ObjectDetection} domain objects representing
|
||||
* detected objects
|
||||
*/
|
||||
public List<ObjectDetection> detect(String imageUri) {
|
||||
try (InputStream is = new DefaultResourceLoader().getResource(imageUri).getInputStream()) {
|
||||
@@ -116,10 +129,11 @@ public class ObjectDetectionService {
|
||||
|
||||
/**
|
||||
* Detects objects in a single {@link BufferedImage}.
|
||||
*
|
||||
* @param image Input image to detect objects from.
|
||||
* @param format Image format (e.g. jpg, png ...) to use when converting the buffer into byte array.
|
||||
* @return Returns a list of {@link ObjectDetection} domain objects representing detected objects in the input image
|
||||
* @param format Image format (e.g. jpg, png ...) to use when converting the buffer
|
||||
* into byte array.
|
||||
* @return Returns a list of {@link ObjectDetection} domain objects representing
|
||||
* detected objects in the input image
|
||||
*/
|
||||
public List<ObjectDetection> detect(BufferedImage image, String format) {
|
||||
return this.detect(GraphicsUtils.toImageByteArray(image, format));
|
||||
@@ -127,21 +141,27 @@ public class ObjectDetectionService {
|
||||
|
||||
/**
|
||||
* Detects objects from a single input image encoded as byte array.
|
||||
*
|
||||
* @param image Input image encoded as byte array
|
||||
* @return Returns a list of {@link ObjectDetection} domain objects representing detected objects in the input image
|
||||
* @return Returns a list of {@link ObjectDetection} domain objects representing
|
||||
* detected objects in the input image
|
||||
*/
|
||||
public List<ObjectDetection> detect(byte[] image) {
|
||||
return this.inputConverter.andThen(this.tensorFlowService).andThen(this.outputConverter).apply(new byte[][] { image }).get(0);
|
||||
return this.inputConverter.andThen(this.tensorFlowService)
|
||||
.andThen(this.outputConverter)
|
||||
.apply(new byte[][] { image })
|
||||
.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses detects objects from a batch of input images encoded as byte array.
|
||||
*
|
||||
* @param batchedImages Batch of input images encoded as byte arrays. First dimension is the batch size and second the image bytes.
|
||||
* @return Returns list of lists. For every input image in the batch a list of {@link ObjectDetection} domain objects representing detected objects in the input image.
|
||||
* @param batchedImages Batch of input images encoded as byte arrays. First dimension
|
||||
* is the batch size and second the image bytes.
|
||||
* @return Returns list of lists. For every input image in the batch a list of
|
||||
* {@link ObjectDetection} domain objects representing detected objects in the input
|
||||
* image.
|
||||
*/
|
||||
public List<List<ObjectDetection>> detect(byte[][] batchedImages) {
|
||||
return this.inputConverter.andThen(this.tensorFlowService).andThen(this.outputConverter).apply(batchedImages);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,35 +40,34 @@ import org.springframework.core.io.DefaultResourceLoader;
|
||||
public class ObjectDetectionService2 implements AutoCloseable {
|
||||
|
||||
/** Default Box models fetch names. */
|
||||
public static List<String> FETCH_NAMES = Arrays.asList(
|
||||
ObjectDetectionOutputConverter.DETECTION_SCORES, ObjectDetectionOutputConverter.DETECTION_CLASSES,
|
||||
ObjectDetectionOutputConverter.DETECTION_BOXES, ObjectDetectionOutputConverter.NUM_DETECTIONS);
|
||||
|
||||
/** Default Models models fetch names. */
|
||||
public static List<String> FETCH_NAMES_WITH_MASKS = Arrays.asList(
|
||||
ObjectDetectionOutputConverter.DETECTION_SCORES, ObjectDetectionOutputConverter.DETECTION_CLASSES,
|
||||
ObjectDetectionOutputConverter.DETECTION_BOXES, ObjectDetectionOutputConverter.DETECTION_MASKS,
|
||||
public static List<String> FETCH_NAMES = Arrays.asList(ObjectDetectionOutputConverter.DETECTION_SCORES,
|
||||
ObjectDetectionOutputConverter.DETECTION_CLASSES, ObjectDetectionOutputConverter.DETECTION_BOXES,
|
||||
ObjectDetectionOutputConverter.NUM_DETECTIONS);
|
||||
|
||||
private final GraphRunner imageNormalization;
|
||||
private final GraphRunner objectDetection;
|
||||
private final ObjectDetectionOutputConverter outputConverter;
|
||||
/** Default Models models fetch names. */
|
||||
public static List<String> FETCH_NAMES_WITH_MASKS = Arrays.asList(ObjectDetectionOutputConverter.DETECTION_SCORES,
|
||||
ObjectDetectionOutputConverter.DETECTION_CLASSES, ObjectDetectionOutputConverter.DETECTION_BOXES,
|
||||
ObjectDetectionOutputConverter.DETECTION_MASKS, ObjectDetectionOutputConverter.NUM_DETECTIONS);
|
||||
|
||||
private final GraphRunner imageNormalization;
|
||||
|
||||
private final GraphRunner objectDetection;
|
||||
|
||||
private final ObjectDetectionOutputConverter outputConverter;
|
||||
|
||||
public ObjectDetectionService2(String modelUri, ObjectDetectionOutputConverter outputConverter) {
|
||||
|
||||
this.imageNormalization = new GraphRunner("raw_image", "normalized_image")
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<String> rawImage = tf.withName("raw_image").placeholder(String.class);
|
||||
Operand<UInt8> decodedImage = tf.dtypes.cast(
|
||||
tf.image.decodeJpeg(rawImage, DecodeJpeg.channels(3L)), UInt8.class);
|
||||
// Expand dimensions since the model expects images to have shape: [1, H, W, 3]
|
||||
tf.withName("normalized_image").expandDims(decodedImage, tf.constant(0));
|
||||
});
|
||||
this.imageNormalization = new GraphRunner("raw_image", "normalized_image").withGraphDefinition(tf -> {
|
||||
Placeholder<String> rawImage = tf.withName("raw_image").placeholder(String.class);
|
||||
Operand<UInt8> decodedImage = tf.dtypes.cast(tf.image.decodeJpeg(rawImage, DecodeJpeg.channels(3L)),
|
||||
UInt8.class);
|
||||
// Expand dimensions since the model expects images to have shape: [1, H, W,
|
||||
// 3]
|
||||
tf.withName("normalized_image").expandDims(decodedImage, tf.constant(0));
|
||||
});
|
||||
|
||||
this.objectDetection = new GraphRunner(Arrays.asList("image_tensor"), FETCH_NAMES)
|
||||
.withGraphDefinition(new ProtoBufGraphDefinition(
|
||||
new DefaultResourceLoader().getResource(modelUri), true));
|
||||
.withGraphDefinition(new ProtoBufGraphDefinition(new DefaultResourceLoader().getResource(modelUri), true));
|
||||
|
||||
this.outputConverter = outputConverter;
|
||||
}
|
||||
@@ -77,9 +76,10 @@ public class ObjectDetectionService2 implements AutoCloseable {
|
||||
try (Tensor inputTensor = Tensor.create(image); GraphRunnerMemory memorize = new GraphRunnerMemory()) {
|
||||
|
||||
List<List<ObjectDetection>> out = this.imageNormalization.andThen(memorize)
|
||||
.andThen(this.objectDetection).andThen(memorize)
|
||||
.andThen(outputConverter)
|
||||
.apply(Collections.singletonMap("raw_image", inputTensor));
|
||||
.andThen(this.objectDetection)
|
||||
.andThen(memorize)
|
||||
.andThen(outputConverter)
|
||||
.apply(Collections.singletonMap("raw_image", inputTensor));
|
||||
|
||||
return out.get(0);
|
||||
|
||||
@@ -90,7 +90,7 @@ public class ObjectDetectionService2 implements AutoCloseable {
|
||||
public void close() {
|
||||
this.imageNormalization.close();
|
||||
this.objectDetection.close();
|
||||
//this.outputConverter.close();
|
||||
// this.outputConverter.close();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
@@ -100,7 +100,8 @@ public class ObjectDetectionService2 implements AutoCloseable {
|
||||
ObjectDetectionOutputConverter outputAdapter = new ObjectDetectionOutputConverter(
|
||||
new DefaultResourceLoader().getResource(labelUri), 0.4f, FETCH_NAMES);
|
||||
|
||||
//byte[] inputImage = GraphicsUtils.loadAsByteArray("classpath:/images/object-detection.jpg");
|
||||
// byte[] inputImage =
|
||||
// GraphicsUtils.loadAsByteArray("classpath:/images/object-detection.jpg");
|
||||
byte[] inputImage = GraphicsUtils.loadAsByteArray("classpath:/images/wild-animals-15.jpg");
|
||||
|
||||
try (ObjectDetectionService2 objectDetectionService2 = new ObjectDetectionService2(modelUri, outputAdapter)) {
|
||||
@@ -110,4 +111,5 @@ public class ObjectDetectionService2 implements AutoCloseable {
|
||||
System.out.println(boza);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,12 +28,19 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
public class ObjectDetection {
|
||||
|
||||
private String name;
|
||||
|
||||
private float confidence;
|
||||
|
||||
private float x1;
|
||||
|
||||
private float y1;
|
||||
|
||||
private float x2;
|
||||
|
||||
private float y2;
|
||||
|
||||
private float[][] mask;
|
||||
|
||||
private int cid;
|
||||
|
||||
public String getName() {
|
||||
@@ -102,15 +109,8 @@ public class ObjectDetection {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ObjectDetection{" +
|
||||
"name='" + name + '\'' +
|
||||
", confidence=" + confidence +
|
||||
", x1=" + x1 +
|
||||
", y1=" + y1 +
|
||||
", x2=" + x2 +
|
||||
", y2=" + y2 +
|
||||
", mask=" + Arrays.toString(mask) +
|
||||
", cid=" + cid +
|
||||
'}';
|
||||
return "ObjectDetection{" + "name='" + name + '\'' + ", confidence=" + confidence + ", x1=" + x1 + ", y1=" + y1
|
||||
+ ", x2=" + x2 + ", y2=" + y2 + ", mask=" + Arrays.toString(mask) + ", cid=" + cid + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,15 +32,15 @@ import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
|
||||
/**
|
||||
* 4 of the pre-trained model in the model zoo (https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md)
|
||||
* 4 of the pre-trained model in the model zoo
|
||||
* (https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md)
|
||||
* can also compute the masks of the detected objects, providing instance segmentation.
|
||||
* <p>
|
||||
* Here are the models that can be used for instance segmentation.
|
||||
* <p>
|
||||
* mask_rcnn_inception_resnet_v2_atrous_coco 771 36 Masks
|
||||
* mask_rcnn_inception_v2_coco 79 25 Masks
|
||||
* mask_rcnn_resnet101_atrous_coco 470 33 Masks
|
||||
* mask_rcnn_resnet50_atrous_coco 343 29 Masks
|
||||
* mask_rcnn_inception_resnet_v2_atrous_coco 771 36 Masks mask_rcnn_inception_v2_coco 79
|
||||
* 25 Masks mask_rcnn_resnet101_atrous_coco 470 33 Masks mask_rcnn_resnet50_atrous_coco
|
||||
* 343 29 Masks
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -50,44 +50,53 @@ public class ExampleInstanceSegmentation {
|
||||
|
||||
ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
// You can download pre-trained models directly from the zoo: https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
// Just use the notation <zoo model tar.gz url>#<name of the frozen model file name>
|
||||
// For performance reasons you may consider downloading the model locally and use the file:/<path to my model> URI instead!
|
||||
// You can download pre-trained models directly from the zoo:
|
||||
// https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
// Just use the notation <zoo model tar.gz url>#<name of the frozen model file
|
||||
// name>
|
||||
// For performance reasons you may consider downloading the model locally and use
|
||||
// the file:/<path to my model> URI instead!
|
||||
String model = "https://download.tensorflow.org/models/object_detection/mask_rcnn_inception_resnet_v2_atrous_coco_2018_01_28.tar.gz#frozen_inference_graph.pb";
|
||||
|
||||
// All labels for the pre-trained models are available at:
|
||||
// https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
// Use the labels applicable for the model.
|
||||
// Also, for performance reasons you may consider to download the labels and load them from file: instead.
|
||||
// Also, for performance reasons you may consider to download the labels and load
|
||||
// them from file: instead.
|
||||
String labels = "https://raw.githubusercontent.com/tensorflow/models/master/research/object_detection/data/mscoco_label_map.pbtxt";
|
||||
|
||||
// You can cache the TF model on the local file system to improve the bootstrap performance on consecutive runs!
|
||||
// You can cache the TF model on the local file system to improve the bootstrap
|
||||
// performance on consecutive runs!
|
||||
boolean CACHE_TF_MODEL = true;
|
||||
|
||||
// For the pre-trained models fromMemory mask you can set the INSTANCE_SEGMENTATION to enable object instance segmentation as well
|
||||
// For the pre-trained models fromMemory mask you can set the
|
||||
// INSTANCE_SEGMENTATION to enable object instance segmentation as well
|
||||
boolean INSTANCE_SEGMENTATION = true;
|
||||
|
||||
// Only object fromMemory confidence above the threshold are returned
|
||||
float CONFIDENCE_THRESHOLD = 0.4f;
|
||||
|
||||
ObjectDetectionService detectionService =
|
||||
new ObjectDetectionService(model, labels, CONFIDENCE_THRESHOLD, INSTANCE_SEGMENTATION, CACHE_TF_MODEL);
|
||||
ObjectDetectionService detectionService = new ObjectDetectionService(model, labels, CONFIDENCE_THRESHOLD,
|
||||
INSTANCE_SEGMENTATION, CACHE_TF_MODEL);
|
||||
|
||||
// You can use file:, http: or classpath: to provide the path to the input image.
|
||||
byte[] image = GraphicsUtils.loadAsByteArray("classpath:/images/object-detection.jpg");
|
||||
|
||||
// Returns a list ObjectDetection domain classes to allow programmatic accesses to the detected objects's metadata
|
||||
// Returns a list ObjectDetection domain classes to allow programmatic accesses to
|
||||
// the detected objects's metadata
|
||||
List<ObjectDetection> detectedObjects = detectionService.detect(image);
|
||||
|
||||
// Get JSON representation of the detected objects
|
||||
String jsonObjectDetections = new JsonMapperFunction().apply(detectedObjects);
|
||||
System.out.println(jsonObjectDetections);
|
||||
|
||||
// Draw the detected object metadata on top of the original image and store the result
|
||||
// Draw the detected object metadata on top of the original image and store the
|
||||
// result
|
||||
byte[] annotatedImage = new ObjectDetectionImageAugmenter(INSTANCE_SEGMENTATION).apply(image, detectedObjects);
|
||||
File projectDir = new File("functions/function/object-detection-function");
|
||||
File output = new File(projectDir, "target/object-detection-segmentation-augmented.jpg");
|
||||
IOUtils.write(annotatedImage, new FileOutputStream(output));
|
||||
System.out.println("Created:" + output.getPath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,31 +37,40 @@ public class ExampleObjectDetection {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
// You can download pre-trained models directly from the zoo: https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
// Just use the notation <zoo model tar.gz url>#<name of the frozen model file name>
|
||||
// For performance reasons you may consider downloading the model locally and use the file:/<path to my model> URI instead!
|
||||
// You can download pre-trained models directly from the zoo:
|
||||
// https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
// Just use the notation <zoo model tar.gz url>#<name of the frozen model file
|
||||
// name>
|
||||
// For performance reasons you may consider downloading the model locally and use
|
||||
// the file:/<path to my model> URI instead!
|
||||
String model = "https://download.tensorflow.org/models/object_detection/faster_rcnn_nas_coco_2018_01_28.tar.gz#frozen_inference_graph.pb";
|
||||
//Resource model = resourceLoader.getResource("https://download.tensorflow.org/models/object_detection/faster_rcnn_resnet101_fgvc_2018_07_19.tar.gz#frozen_inference_graph.pb");
|
||||
//Resource model = resourceLoader.getResource("https://download.tensorflow.org/models/object_detection/faster_rcnn_resnet50_fgvc_2018_07_19.tar.gz#frozen_inference_graph.pb");
|
||||
// Resource model =
|
||||
// resourceLoader.getResource("https://download.tensorflow.org/models/object_detection/faster_rcnn_resnet101_fgvc_2018_07_19.tar.gz#frozen_inference_graph.pb");
|
||||
// Resource model =
|
||||
// resourceLoader.getResource("https://download.tensorflow.org/models/object_detection/faster_rcnn_resnet50_fgvc_2018_07_19.tar.gz#frozen_inference_graph.pb");
|
||||
|
||||
// All labels for the pre-trained models are available at:
|
||||
// https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
// Use the labels applicable for the model.
|
||||
// Also, for performance reasons you may consider to download the labels and load them from file: instead.
|
||||
// Also, for performance reasons you may consider to download the labels and load
|
||||
// them from file: instead.
|
||||
String labels = "https://raw.githubusercontent.com/tensorflow/models/master/research/object_detection/data/mscoco_label_map.pbtxt";
|
||||
//Resource labels = resourceLoader.getResource("https://raw.githubusercontent.com/tensorflow/models/master/research/object_detection/data/fgvc_2854_classes_label_map.pbtxt");
|
||||
// Resource labels =
|
||||
// resourceLoader.getResource("https://raw.githubusercontent.com/tensorflow/models/master/research/object_detection/data/fgvc_2854_classes_label_map.pbtxt");
|
||||
|
||||
// You can cache the TF model on the local file system to improve the bootstrap performance on consecutive runs!
|
||||
// You can cache the TF model on the local file system to improve the bootstrap
|
||||
// performance on consecutive runs!
|
||||
boolean CACHE_TF_MODEL = true;
|
||||
|
||||
// For the pre-trained models fromMemory mask you can set the INSTANCE_SEGMENTATION to enable object instance segmentation as well
|
||||
// For the pre-trained models fromMemory mask you can set the
|
||||
// INSTANCE_SEGMENTATION to enable object instance segmentation as well
|
||||
boolean NO_INSTANCE_SEGMENTATION = false;
|
||||
|
||||
// Only object fromMemory confidence above the threshold are returned
|
||||
float CONFIDENCE_THRESHOLD = 0.4f;
|
||||
|
||||
ObjectDetectionService detectionService =
|
||||
new ObjectDetectionService(model, labels, CONFIDENCE_THRESHOLD, NO_INSTANCE_SEGMENTATION, CACHE_TF_MODEL);
|
||||
ObjectDetectionService detectionService = new ObjectDetectionService(model, labels, CONFIDENCE_THRESHOLD,
|
||||
NO_INSTANCE_SEGMENTATION, CACHE_TF_MODEL);
|
||||
|
||||
// You can use file:, http: or classpath: to provide the path to the input image.
|
||||
String inputImageUri = "classpath:/images/object-detection.jpg";
|
||||
@@ -69,16 +78,21 @@ public class ExampleObjectDetection {
|
||||
|
||||
byte[] image = StreamUtils.copyToByteArray(is);
|
||||
|
||||
// Returns a list ObjectDetection domain classes to allow programmatic accesses to the detected objects's metadata
|
||||
// Returns a list ObjectDetection domain classes to allow programmatic
|
||||
// accesses to the detected objects's metadata
|
||||
List<ObjectDetection> detectedObjects = detectionService.detect(image);
|
||||
|
||||
// Get JSON representation of the detected objects
|
||||
String jsonObjectDetections = new JsonMapperFunction().apply(detectedObjects);
|
||||
System.out.println(jsonObjectDetections);
|
||||
|
||||
// Draw the detected object metadata on top of the original image and store the result
|
||||
byte[] annotatedImage = new ObjectDetectionImageAugmenter(NO_INSTANCE_SEGMENTATION).apply(image, detectedObjects);
|
||||
IOUtils.write(annotatedImage, new FileOutputStream("./object-detection-function/target/object-detection-augmented.jpg"));
|
||||
// Draw the detected object metadata on top of the original image and store
|
||||
// the result
|
||||
byte[] annotatedImage = new ObjectDetectionImageAugmenter(NO_INSTANCE_SEGMENTATION).apply(image,
|
||||
detectedObjects);
|
||||
IOUtils.write(annotatedImage,
|
||||
new FileOutputStream("./object-detection-function/target/object-detection-augmented.jpg"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.cloud.fn.object.detection.examples;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.fn.object.detection.ObjectDetectionService;
|
||||
@@ -28,15 +27,30 @@ import org.springframework.cloud.fn.object.detection.domain.ObjectDetection;
|
||||
public class SimpleExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Select a pre-trained model from the model zoo: https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
// Just use the notation <model zoo url>#<name of the frozen model file in the zoo's tar.gz>
|
||||
// Select a pre-trained model from the model zoo:
|
||||
// https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
|
||||
// Just use the notation <model zoo url>#<name of the frozen model file in the
|
||||
// zoo's tar.gz>
|
||||
String model = "https://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_ppn_shared_box_predictor_300x300_coco14_sync_2018_07_03.tar.gz#frozen_inference_graph.pb";
|
||||
|
||||
// All labels for the pre-trained models are available at: https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
// All labels for the pre-trained models are available at:
|
||||
// https://github.com/tensorflow/models/tree/master/research/object_detection/data
|
||||
String labels = "https://raw.githubusercontent.com/tensorflow/models/master/research/object_detection/data/mscoco_label_map.pbtxt";
|
||||
|
||||
ObjectDetectionService detectionService = new ObjectDetectionService(model, labels,
|
||||
0.4f, // Only object fromMemory confidence above the threshold are returned. Confidence range is [0, 1].
|
||||
ObjectDetectionService detectionService = new ObjectDetectionService(model, labels, 0.4f, // Only
|
||||
// object
|
||||
// fromMemory
|
||||
// confidence
|
||||
// above
|
||||
// the
|
||||
// threshold
|
||||
// are
|
||||
// returned.
|
||||
// Confidence
|
||||
// range
|
||||
// is
|
||||
// [0,
|
||||
// 1].
|
||||
false, // No instance segmentation
|
||||
true); // cache the TF model locally
|
||||
|
||||
@@ -44,4 +58,5 @@ public class SimpleExample {
|
||||
List<ObjectDetection> detectedObjects = detectionService.detect("classpath:/images/object-detection.jpg");
|
||||
detectedObjects.stream().map(o -> o.toString()).forEach(System.out::println);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class ByteArrayTextToString implements Function<Message<?>, Message<?>> {
|
||||
@@ -35,17 +34,17 @@ public class ByteArrayTextToString implements Function<Message<?>, Message<?>> {
|
||||
if (message.getPayload() instanceof byte[]) {
|
||||
final MessageHeaders headers = message.getHeaders();
|
||||
String contentType = headers.containsKey(MessageHeaders.CONTENT_TYPE)
|
||||
? headers.get(MessageHeaders.CONTENT_TYPE).toString()
|
||||
: MimeTypeUtils.APPLICATION_JSON_VALUE;
|
||||
? headers.get(MessageHeaders.CONTENT_TYPE).toString() : MimeTypeUtils.APPLICATION_JSON_VALUE;
|
||||
|
||||
if (contentType.contains("text") || contentType.contains("json") || contentType.contains("x-spring-tuple")) {
|
||||
if (contentType.contains("text") || contentType.contains("json")
|
||||
|| contentType.contains("x-spring-tuple")) {
|
||||
message = MessageBuilder.withPayload(new String(((byte[]) message.getPayload())))
|
||||
.copyHeaders(message.getHeaders())
|
||||
.build();
|
||||
.copyHeaders(message.getHeaders())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,12 +29,11 @@ import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* The {@link Function} to deserialize {@code byte[]} payload into a Map
|
||||
* if {@link MessageHeaders#CONTENT_TYPE} header is JSON.
|
||||
* Otherwise, the message is returned as is.
|
||||
* The {@link Function} to deserialize {@code byte[]} payload into a Map if
|
||||
* {@link MessageHeaders#CONTENT_TYPE} header is JSON. Otherwise, the message is returned
|
||||
* as is.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 4.0
|
||||
*/
|
||||
public class JsonBytesToMap implements Function<Message<?>, Message<?>> {
|
||||
@@ -51,15 +50,13 @@ public class JsonBytesToMap implements Function<Message<?>, Message<?>> {
|
||||
public Message<?> apply(Message<?> message) {
|
||||
if (message.getPayload() instanceof byte[] payload) {
|
||||
MessageHeaders headers = message.getHeaders();
|
||||
String contentType =
|
||||
headers.containsKey(MessageHeaders.CONTENT_TYPE)
|
||||
? headers.get(MessageHeaders.CONTENT_TYPE).toString()
|
||||
: MimeTypeUtils.APPLICATION_JSON_VALUE;
|
||||
String contentType = headers.containsKey(MessageHeaders.CONTENT_TYPE)
|
||||
? headers.get(MessageHeaders.CONTENT_TYPE).toString() : MimeTypeUtils.APPLICATION_JSON_VALUE;
|
||||
|
||||
if (contentType.contains("json")) {
|
||||
message = MessageBuilder.withPayload(payloadToMapIfCan(payload))
|
||||
.copyHeaders(message.getHeaders())
|
||||
.build();
|
||||
.copyHeaders(message.getHeaders())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class ByteArrayTextToStringTests {
|
||||
|
||||
private static final String MESSAGE = "hello world";
|
||||
|
||||
private static Function<Message<?>, Message<?>> converter;
|
||||
|
||||
@BeforeAll
|
||||
|
||||
@@ -43,11 +43,10 @@ public final class NativeImageUtils {
|
||||
* https://github.com/tensorflow/tensorflow/blob/r1.13/tensorflow/python/ops/image_ops_impl.py#L1536
|
||||
*/
|
||||
public static <T> Operand<T> grayscaleToRgb(Ops tf, Operand<T> images) {
|
||||
ExpandDims<Integer> rank_1 = tf.expandDims(
|
||||
tf.math.sub(tf.rank(images), tf.constant(1)),
|
||||
tf.constant(0));
|
||||
ExpandDims<Integer> rank_1 = tf.expandDims(tf.math.sub(tf.rank(images), tf.constant(1)), tf.constant(0));
|
||||
// Create once 1D vector of the shape defined by the rank_1.
|
||||
// E.g. for rank [2] will produce matrix [1, 1]. For [3] rank will produce a cube [1, 1, 1]
|
||||
// E.g. for rank [2] will produce matrix [1, 1]. For [3] rank will produce a cube
|
||||
// [1, 1, 1]
|
||||
Add<Integer> ones = tf.math.add(tf.zeros(rank_1, Integer.class), tf.constant(1));
|
||||
// Convert scalar 3 into 1D array [3]
|
||||
ExpandDims<Integer> channelsAs1D = tf.expandDims(tf.constant(3), tf.constant(0));
|
||||
@@ -59,49 +58,57 @@ public final class NativeImageUtils {
|
||||
public static Operand<Float> normalizeMask(Ops tf, Operand<Float> mask, float newValue) {
|
||||
// generate array representing the axis indexes.
|
||||
// For example of tensor of rank K the axisRange is {0, 1, 2 ...K}
|
||||
Range<Integer> axisRange = tf.range(tf.constant(0), // from
|
||||
Range<Integer> axisRange = tf.range(tf.constant(0), // from
|
||||
tf.dtypes.cast(tf.rank(mask), Integer.class), // to
|
||||
tf.constant(1)); // step
|
||||
|
||||
ReduceMax<Float> max = tf.reduceMax(mask, axisRange);
|
||||
//Mul<Float> input2Float1 = tf.math.mul(tf.math.div(input2Float, max), tf.constant(1f));
|
||||
// Mul<Float> input2Float1 = tf.math.mul(tf.math.div(input2Float, max),
|
||||
// tf.constant(1f));
|
||||
Mul<Float> normalizedMask = tf.math.mul(tf.math.div(mask, max), tf.constant(newValue));
|
||||
|
||||
return normalizedMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha Blending .
|
||||
* https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
|
||||
* Alpha Blending . https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
|
||||
*/
|
||||
public static Operand<Float> alphaBlending(Ops tf, Operand<Float> srcRgb, Operand<Float> dstRgb, Operand<Float> srcAlpha) {
|
||||
public static Operand<Float> alphaBlending(Ops tf, Operand<Float> srcRgb, Operand<Float> dstRgb,
|
||||
Operand<Float> srcAlpha) {
|
||||
Sub<Float> alpha = tf.math.sub(tf.onesLike(srcRgb), srcAlpha);
|
||||
Mul<Float> src = tf.math.mul(srcRgb, alpha);
|
||||
Mul<Float> dst = tf.math.mul(dstRgb, tf.math.sub(tf.constant(1.0f), alpha));
|
||||
Add<Float> out = tf.math.add(dst, src);
|
||||
|
||||
//Mul<Float> out = tf.math.mul(srcRgbNormalized, dstRgb);
|
||||
//Squeeze<Float> squeeze = tf.withName("squeeze").squeeze(out, Squeeze.axis(Arrays.asList(0L)));
|
||||
// Mul<Float> out = tf.math.mul(srcRgbNormalized, dstRgb);
|
||||
// Squeeze<Float> squeeze = tf.withName("squeeze").squeeze(out,
|
||||
// Squeeze.axis(Arrays.asList(0L)));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mask can contain label values larger than the list of colors provided in the color map.
|
||||
* To avoid out-of-index errors we will "normalize" the label values in the mask to MOD max-color-table-value.
|
||||
* The mask can contain label values larger than the list of colors provided in the
|
||||
* color map. To avoid out-of-index errors we will "normalize" the label values in the
|
||||
* mask to MOD max-color-table-value.
|
||||
* @param tf - tensorflow
|
||||
* @param colorTable Color map of shape [n, 3]. n is the count of label entries and 3 is the RGB color assigned
|
||||
* to that label.
|
||||
* @param colorTable Color map of shape [n, 3]. n is the count of label entries and 3
|
||||
* is the RGB color assigned to that label.
|
||||
* @param mask Mask of shape [h, w] containing label vales.
|
||||
* @return Mask of shape [h, w] fromMemory values normalized between [0, n]
|
||||
*/
|
||||
public static Operand<Long> normalizeMaskLabels(Ops tf, Operand<Integer> colorTable, Operand<Long> mask) {
|
||||
// The mask can contain label values larger than the list of colors provided in the color map.
|
||||
// To avoid out-of-index errors we will "normalize" the label values in the mask to MOD max-color-table-value.
|
||||
// The mask can contain label values larger than the list of colors provided in
|
||||
// the color map.
|
||||
// To avoid out-of-index errors we will "normalize" the label values in the mask
|
||||
// to MOD max-color-table-value.
|
||||
Sub<Long> colorTableShape = tf.math.sub(tf.shape(colorTable, Long.class), tf.constant(1L));
|
||||
// Color tables have shape [N, 3], where N is the count of label entries. Therefore the max label id is (N - 1).
|
||||
// Color tables have shape [N, 3], where N is the count of label entries.
|
||||
// Therefore the max label id is (N - 1).
|
||||
Gather<Long> colorTableSize = tf.gather(colorTableShape, tf.constant(new int[] { 0 }), tf.constant(0));
|
||||
// Normalize the label values in the mask so they don't exceed the max value in the color map.
|
||||
// Normalize the label values in the mask so they don't exceed the max value in
|
||||
// the color map.
|
||||
return tf.math.mod(mask, colorTableSize);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,16 +25,17 @@ import org.springframework.core.io.DefaultResourceLoader;
|
||||
|
||||
/**
|
||||
*
|
||||
* Visualizes the segmentation results via specified color map.
|
||||
* Color maps helping to visualize the semantic segmentation results for the different datasets.
|
||||
* Visualizes the segmentation results via specified color map. Color maps helping to
|
||||
* visualize the semantic segmentation results for the different datasets.
|
||||
*
|
||||
* Supported colormaps are:
|
||||
* - ADE20K (http://groups.csail.mit.edu/vision/datasets/ADE20K/).
|
||||
* - Cityscapes dataset (https://www.cityscapes-dataset.com).
|
||||
* - Mapillary Vistas (https://research.mapillary.com).
|
||||
* - PASCAL VOC 2012 (http://host.robots.ox.ac.uk/pascal/VOC/).
|
||||
* Supported colormaps are: - ADE20K
|
||||
* (http://groups.csail.mit.edu/vision/datasets/ADE20K/). - Cityscapes dataset
|
||||
* (https://www.cityscapes-dataset.com). - Mapillary Vistas
|
||||
* (https://research.mapillary.com). - PASCAL VOC 2012
|
||||
* (http://host.robots.ox.ac.uk/pascal/VOC/).
|
||||
*
|
||||
* Based on: https://github.com/tensorflow/models/blob/master/research/deeplab/utils/get_dataset_colormap.py
|
||||
* Based on:
|
||||
* https://github.com/tensorflow/models/blob/master/research/deeplab/utils/get_dataset_colormap.py
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -45,238 +46,51 @@ public final class SegmentationColorMap {
|
||||
}
|
||||
|
||||
/** MAPILLARY_COLORMAP . */
|
||||
public static final int[][] MAPILLARY_COLORMAP = new int[][] {
|
||||
{ 165, 42, 42 },
|
||||
{ 0, 192, 0 },
|
||||
{ 196, 196, 196 },
|
||||
{ 190, 153, 153 },
|
||||
{ 180, 165, 180 },
|
||||
{ 102, 102, 156 },
|
||||
{ 102, 102, 156 },
|
||||
{ 128, 64, 255 },
|
||||
{ 140, 140, 200 },
|
||||
{ 170, 170, 170 },
|
||||
{ 250, 170, 160 },
|
||||
{ 96, 96, 96 },
|
||||
{ 230, 150, 140 },
|
||||
{ 128, 64, 128 },
|
||||
{ 110, 110, 110 },
|
||||
{ 244, 35, 232 },
|
||||
{ 150, 100, 100 },
|
||||
{ 70, 70, 70 },
|
||||
{ 150, 120, 90 },
|
||||
{ 220, 20, 60 },
|
||||
{ 255, 0, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 200, 128, 128 },
|
||||
{ 255, 255, 255 },
|
||||
{ 64, 170, 64 },
|
||||
{ 128, 64, 64 },
|
||||
{ 70, 130, 180 },
|
||||
{ 255, 255, 255 },
|
||||
{ 152, 251, 152 },
|
||||
{ 107, 142, 35 },
|
||||
{ 0, 170, 30 },
|
||||
{ 255, 255, 128 },
|
||||
{ 250, 0, 30 },
|
||||
{ 0, 0, 0 },
|
||||
{ 220, 220, 220 },
|
||||
{ 170, 170, 170 },
|
||||
{ 222, 40, 40 },
|
||||
{ 100, 170, 30 },
|
||||
{ 40, 40, 40 },
|
||||
{ 33, 33, 33 },
|
||||
{ 170, 170, 170 },
|
||||
{ 0, 0, 142 },
|
||||
{ 170, 170, 170 },
|
||||
{ 210, 170, 100 },
|
||||
{ 153, 153, 153 },
|
||||
{ 128, 128, 128 },
|
||||
{ 0, 0, 142 },
|
||||
{ 250, 170, 30 },
|
||||
{ 192, 192, 192 },
|
||||
{ 220, 220, 0 },
|
||||
{ 180, 165, 180 },
|
||||
{ 119, 11, 32 },
|
||||
{ 0, 0, 142 },
|
||||
{ 0, 60, 100 },
|
||||
{ 0, 0, 142 },
|
||||
{ 0, 0, 90 },
|
||||
{ 0, 0, 230 },
|
||||
{ 0, 80, 100 },
|
||||
{ 128, 64, 64 },
|
||||
{ 0, 0, 110 },
|
||||
{ 0, 0, 70 },
|
||||
{ 0, 0, 192 },
|
||||
{ 32, 32, 32 },
|
||||
{ 0, 0, 0 },
|
||||
{ 0, 0, 0 },
|
||||
};
|
||||
public static final int[][] MAPILLARY_COLORMAP = new int[][] { { 165, 42, 42 }, { 0, 192, 0 }, { 196, 196, 196 },
|
||||
{ 190, 153, 153 }, { 180, 165, 180 }, { 102, 102, 156 }, { 102, 102, 156 }, { 128, 64, 255 },
|
||||
{ 140, 140, 200 }, { 170, 170, 170 }, { 250, 170, 160 }, { 96, 96, 96 }, { 230, 150, 140 },
|
||||
{ 128, 64, 128 }, { 110, 110, 110 }, { 244, 35, 232 }, { 150, 100, 100 }, { 70, 70, 70 }, { 150, 120, 90 },
|
||||
{ 220, 20, 60 }, { 255, 0, 0 }, { 255, 0, 0 }, { 255, 0, 0 }, { 200, 128, 128 }, { 255, 255, 255 },
|
||||
{ 64, 170, 64 }, { 128, 64, 64 }, { 70, 130, 180 }, { 255, 255, 255 }, { 152, 251, 152 }, { 107, 142, 35 },
|
||||
{ 0, 170, 30 }, { 255, 255, 128 }, { 250, 0, 30 }, { 0, 0, 0 }, { 220, 220, 220 }, { 170, 170, 170 },
|
||||
{ 222, 40, 40 }, { 100, 170, 30 }, { 40, 40, 40 }, { 33, 33, 33 }, { 170, 170, 170 }, { 0, 0, 142 },
|
||||
{ 170, 170, 170 }, { 210, 170, 100 }, { 153, 153, 153 }, { 128, 128, 128 }, { 0, 0, 142 }, { 250, 170, 30 },
|
||||
{ 192, 192, 192 }, { 220, 220, 0 }, { 180, 165, 180 }, { 119, 11, 32 }, { 0, 0, 142 }, { 0, 60, 100 },
|
||||
{ 0, 0, 142 }, { 0, 0, 90 }, { 0, 0, 230 }, { 0, 80, 100 }, { 128, 64, 64 }, { 0, 0, 110 }, { 0, 0, 70 },
|
||||
{ 0, 0, 192 }, { 32, 32, 32 }, { 0, 0, 0 }, { 0, 0, 0 }, };
|
||||
|
||||
/**
|
||||
* Label colormap used in ADE20K segmentation benchmark.
|
||||
*/
|
||||
public static final int[][] ADE20K_COLORMAP = new int[][] {
|
||||
{ 0, 0, 0 },
|
||||
{ 120, 120, 120 },
|
||||
{ 180, 120, 120 },
|
||||
{ 6, 230, 230 },
|
||||
{ 80, 50, 50 },
|
||||
{ 4, 200, 3 },
|
||||
{ 120, 120, 80 },
|
||||
{ 140, 140, 140 },
|
||||
{ 204, 5, 255 },
|
||||
{ 230, 230, 230 },
|
||||
{ 4, 250, 7 },
|
||||
{ 224, 5, 255 },
|
||||
{ 235, 255, 7 },
|
||||
{ 150, 5, 61 },
|
||||
{ 120, 120, 70 },
|
||||
{ 8, 255, 51 },
|
||||
{ 255, 6, 82 },
|
||||
{ 143, 255, 140 },
|
||||
{ 204, 255, 4 },
|
||||
{ 255, 51, 7 },
|
||||
{ 204, 70, 3 },
|
||||
{ 0, 102, 200 },
|
||||
{ 61, 230, 250 },
|
||||
{ 255, 6, 51 },
|
||||
{ 11, 102, 255 },
|
||||
{ 255, 7, 71 },
|
||||
{ 255, 9, 224 },
|
||||
{ 9, 7, 230 },
|
||||
{ 220, 220, 220 },
|
||||
{ 255, 9, 92 },
|
||||
{ 112, 9, 255 },
|
||||
{ 8, 255, 214 },
|
||||
{ 7, 255, 224 },
|
||||
{ 255, 184, 6 },
|
||||
{ 10, 255, 71 },
|
||||
{ 255, 41, 10 },
|
||||
{ 7, 255, 255 },
|
||||
{ 224, 255, 8 },
|
||||
{ 102, 8, 255 },
|
||||
{ 255, 61, 6 },
|
||||
{ 255, 194, 7 },
|
||||
{ 255, 122, 8 },
|
||||
{ 0, 255, 20 },
|
||||
{ 255, 8, 41 },
|
||||
{ 255, 5, 153 },
|
||||
{ 6, 51, 255 },
|
||||
{ 235, 12, 255 },
|
||||
{ 160, 150, 20 },
|
||||
{ 0, 163, 255 },
|
||||
{ 140, 140, 140 },
|
||||
{ 250, 10, 15 },
|
||||
{ 20, 255, 0 },
|
||||
{ 31, 255, 0 },
|
||||
{ 255, 31, 0 },
|
||||
{ 255, 224, 0 },
|
||||
{ 153, 255, 0 },
|
||||
{ 0, 0, 255 },
|
||||
{ 255, 71, 0 },
|
||||
{ 0, 235, 255 },
|
||||
{ 0, 173, 255 },
|
||||
{ 31, 0, 255 },
|
||||
{ 11, 200, 200 },
|
||||
{ 255, 82, 0 },
|
||||
{ 0, 255, 245 },
|
||||
{ 0, 61, 255 },
|
||||
{ 0, 255, 112 },
|
||||
{ 0, 255, 133 },
|
||||
{ 255, 0, 0 },
|
||||
{ 255, 163, 0 },
|
||||
{ 255, 102, 0 },
|
||||
{ 194, 255, 0 },
|
||||
{ 0, 143, 255 },
|
||||
{ 51, 255, 0 },
|
||||
{ 0, 82, 255 },
|
||||
{ 0, 255, 41 },
|
||||
{ 0, 255, 173 },
|
||||
{ 10, 0, 255 },
|
||||
{ 173, 255, 0 },
|
||||
{ 0, 255, 153 },
|
||||
{ 255, 92, 0 },
|
||||
{ 255, 0, 255 },
|
||||
{ 255, 0, 245 },
|
||||
{ 255, 0, 102 },
|
||||
{ 255, 173, 0 },
|
||||
{ 255, 0, 20 },
|
||||
{ 255, 184, 184 },
|
||||
{ 0, 31, 255 },
|
||||
{ 0, 255, 61 },
|
||||
{ 0, 71, 255 },
|
||||
{ 255, 0, 204 },
|
||||
{ 0, 255, 194 },
|
||||
{ 0, 255, 82 },
|
||||
{ 0, 10, 255 },
|
||||
{ 0, 112, 255 },
|
||||
{ 51, 0, 255 },
|
||||
{ 0, 194, 255 },
|
||||
{ 0, 122, 255 },
|
||||
{ 0, 255, 163 },
|
||||
{ 255, 153, 0 },
|
||||
{ 0, 255, 10 },
|
||||
{ 255, 112, 0 },
|
||||
{ 143, 255, 0 },
|
||||
{ 82, 0, 255 },
|
||||
{ 163, 255, 0 },
|
||||
{ 255, 235, 0 },
|
||||
{ 8, 184, 170 },
|
||||
{ 133, 0, 255 },
|
||||
{ 0, 255, 92 },
|
||||
{ 184, 0, 255 },
|
||||
{ 255, 0, 31 },
|
||||
{ 0, 184, 255 },
|
||||
{ 0, 214, 255 },
|
||||
{ 255, 0, 112 },
|
||||
{ 92, 255, 0 },
|
||||
{ 0, 224, 255 },
|
||||
{ 112, 224, 255 },
|
||||
{ 70, 184, 160 },
|
||||
{ 163, 0, 255 },
|
||||
{ 153, 0, 255 },
|
||||
{ 71, 255, 0 },
|
||||
{ 255, 0, 163 },
|
||||
{ 255, 204, 0 },
|
||||
{ 255, 0, 143 },
|
||||
{ 0, 255, 235 },
|
||||
{ 133, 255, 0 },
|
||||
{ 255, 0, 235 },
|
||||
{ 245, 0, 255 },
|
||||
{ 255, 0, 122 },
|
||||
{ 255, 245, 0 },
|
||||
{ 10, 190, 212 },
|
||||
{ 214, 255, 0 },
|
||||
{ 0, 204, 255 },
|
||||
{ 20, 0, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 0, 153, 255 },
|
||||
{ 0, 41, 255 },
|
||||
{ 0, 255, 204 },
|
||||
{ 41, 0, 255 },
|
||||
{ 41, 255, 0 },
|
||||
{ 173, 0, 255 },
|
||||
{ 0, 245, 255 },
|
||||
{ 71, 0, 255 },
|
||||
{ 122, 0, 255 },
|
||||
{ 0, 255, 184 },
|
||||
{ 0, 92, 255 },
|
||||
{ 184, 255, 0 },
|
||||
{ 0, 133, 255 },
|
||||
{ 255, 214, 0 },
|
||||
{ 25, 194, 194 },
|
||||
{ 102, 255, 0 },
|
||||
{ 92, 0, 255 },
|
||||
};
|
||||
public static final int[][] ADE20K_COLORMAP = new int[][] { { 0, 0, 0 }, { 120, 120, 120 }, { 180, 120, 120 },
|
||||
{ 6, 230, 230 }, { 80, 50, 50 }, { 4, 200, 3 }, { 120, 120, 80 }, { 140, 140, 140 }, { 204, 5, 255 },
|
||||
{ 230, 230, 230 }, { 4, 250, 7 }, { 224, 5, 255 }, { 235, 255, 7 }, { 150, 5, 61 }, { 120, 120, 70 },
|
||||
{ 8, 255, 51 }, { 255, 6, 82 }, { 143, 255, 140 }, { 204, 255, 4 }, { 255, 51, 7 }, { 204, 70, 3 },
|
||||
{ 0, 102, 200 }, { 61, 230, 250 }, { 255, 6, 51 }, { 11, 102, 255 }, { 255, 7, 71 }, { 255, 9, 224 },
|
||||
{ 9, 7, 230 }, { 220, 220, 220 }, { 255, 9, 92 }, { 112, 9, 255 }, { 8, 255, 214 }, { 7, 255, 224 },
|
||||
{ 255, 184, 6 }, { 10, 255, 71 }, { 255, 41, 10 }, { 7, 255, 255 }, { 224, 255, 8 }, { 102, 8, 255 },
|
||||
{ 255, 61, 6 }, { 255, 194, 7 }, { 255, 122, 8 }, { 0, 255, 20 }, { 255, 8, 41 }, { 255, 5, 153 },
|
||||
{ 6, 51, 255 }, { 235, 12, 255 }, { 160, 150, 20 }, { 0, 163, 255 }, { 140, 140, 140 }, { 250, 10, 15 },
|
||||
{ 20, 255, 0 }, { 31, 255, 0 }, { 255, 31, 0 }, { 255, 224, 0 }, { 153, 255, 0 }, { 0, 0, 255 },
|
||||
{ 255, 71, 0 }, { 0, 235, 255 }, { 0, 173, 255 }, { 31, 0, 255 }, { 11, 200, 200 }, { 255, 82, 0 },
|
||||
{ 0, 255, 245 }, { 0, 61, 255 }, { 0, 255, 112 }, { 0, 255, 133 }, { 255, 0, 0 }, { 255, 163, 0 },
|
||||
{ 255, 102, 0 }, { 194, 255, 0 }, { 0, 143, 255 }, { 51, 255, 0 }, { 0, 82, 255 }, { 0, 255, 41 },
|
||||
{ 0, 255, 173 }, { 10, 0, 255 }, { 173, 255, 0 }, { 0, 255, 153 }, { 255, 92, 0 }, { 255, 0, 255 },
|
||||
{ 255, 0, 245 }, { 255, 0, 102 }, { 255, 173, 0 }, { 255, 0, 20 }, { 255, 184, 184 }, { 0, 31, 255 },
|
||||
{ 0, 255, 61 }, { 0, 71, 255 }, { 255, 0, 204 }, { 0, 255, 194 }, { 0, 255, 82 }, { 0, 10, 255 },
|
||||
{ 0, 112, 255 }, { 51, 0, 255 }, { 0, 194, 255 }, { 0, 122, 255 }, { 0, 255, 163 }, { 255, 153, 0 },
|
||||
{ 0, 255, 10 }, { 255, 112, 0 }, { 143, 255, 0 }, { 82, 0, 255 }, { 163, 255, 0 }, { 255, 235, 0 },
|
||||
{ 8, 184, 170 }, { 133, 0, 255 }, { 0, 255, 92 }, { 184, 0, 255 }, { 255, 0, 31 }, { 0, 184, 255 },
|
||||
{ 0, 214, 255 }, { 255, 0, 112 }, { 92, 255, 0 }, { 0, 224, 255 }, { 112, 224, 255 }, { 70, 184, 160 },
|
||||
{ 163, 0, 255 }, { 153, 0, 255 }, { 71, 255, 0 }, { 255, 0, 163 }, { 255, 204, 0 }, { 255, 0, 143 },
|
||||
{ 0, 255, 235 }, { 133, 255, 0 }, { 255, 0, 235 }, { 245, 0, 255 }, { 255, 0, 122 }, { 255, 245, 0 },
|
||||
{ 10, 190, 212 }, { 214, 255, 0 }, { 0, 204, 255 }, { 20, 0, 255 }, { 255, 255, 0 }, { 0, 153, 255 },
|
||||
{ 0, 41, 255 }, { 0, 255, 204 }, { 41, 0, 255 }, { 41, 255, 0 }, { 173, 0, 255 }, { 0, 245, 255 },
|
||||
{ 71, 0, 255 }, { 122, 0, 255 }, { 0, 255, 184 }, { 0, 92, 255 }, { 184, 255, 0 }, { 0, 133, 255 },
|
||||
{ 255, 214, 0 }, { 25, 194, 194 }, { 102, 255, 0 }, { 92, 0, 255 }, };
|
||||
|
||||
/** BLACK_WHITE_COLORMAP . */
|
||||
public static int[][] BLACK_WHITE_COLORMAP = new int[][] {
|
||||
{ 0, 0, 0 },
|
||||
{ 127, 127, 127 },
|
||||
{ 255, 255, 255 },
|
||||
};
|
||||
public static int[][] BLACK_WHITE_COLORMAP = new int[][] { { 0, 0, 0 }, { 127, 127, 127 }, { 255, 255, 255 }, };
|
||||
|
||||
/** CITYMAP_COLORMAP . */
|
||||
public static final int[][] CITYMAP_COLORMAP = new int[255][3];
|
||||
@@ -284,27 +98,10 @@ public final class SegmentationColorMap {
|
||||
static {
|
||||
|
||||
// Initialize citymap
|
||||
int[][] _CITYMAP_COLORMAP = new int[][] {
|
||||
{ 128, 64, 128 },
|
||||
{ 244, 35, 232 },
|
||||
{ 70, 70, 70 },
|
||||
{ 102, 102, 156 },
|
||||
{ 190, 153, 153 },
|
||||
{ 153, 153, 153 },
|
||||
{ 250, 170, 30 },
|
||||
{ 220, 220, 0 },
|
||||
{ 107, 142, 35 },
|
||||
{ 152, 251, 152 },
|
||||
{ 70, 130, 180 },
|
||||
{ 220, 20, 60 },
|
||||
{ 255, 0, 0 },
|
||||
{ 0, 0, 142 },
|
||||
{ 0, 0, 70 },
|
||||
{ 0, 60, 100 },
|
||||
{ 0, 80, 100 },
|
||||
{ 0, 0, 230 },
|
||||
{ 119, 11, 32 }
|
||||
};
|
||||
int[][] _CITYMAP_COLORMAP = new int[][] { { 128, 64, 128 }, { 244, 35, 232 }, { 70, 70, 70 }, { 102, 102, 156 },
|
||||
{ 190, 153, 153 }, { 153, 153, 153 }, { 250, 170, 30 }, { 220, 220, 0 }, { 107, 142, 35 },
|
||||
{ 152, 251, 152 }, { 70, 130, 180 }, { 220, 20, 60 }, { 255, 0, 0 }, { 0, 0, 142 }, { 0, 0, 70 },
|
||||
{ 0, 60, 100 }, { 0, 80, 100 }, { 0, 0, 230 }, { 119, 11, 32 } };
|
||||
|
||||
for (int i = 0; i < _CITYMAP_COLORMAP.length; i++) {
|
||||
System.arraycopy(_CITYMAP_COLORMAP[i], 0, CITYMAP_COLORMAP[i], 0, _CITYMAP_COLORMAP[i].length);
|
||||
@@ -323,8 +120,11 @@ public final class SegmentationColorMap {
|
||||
}
|
||||
|
||||
public static class ColorMap {
|
||||
|
||||
private String name;
|
||||
|
||||
private String info;
|
||||
|
||||
private int[][] colormap;
|
||||
|
||||
public String getName() {
|
||||
@@ -353,11 +153,10 @@ public final class SegmentationColorMap {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ColorMap{" +
|
||||
"name='" + name + '\'' +
|
||||
"info='" + info + '\'' +
|
||||
", colormap=" + Arrays.deepToString(colormap) +
|
||||
'}';
|
||||
return "ColorMap{" + "name='" + name + '\'' + "info='" + info + '\'' + ", colormap="
|
||||
+ Arrays.deepToString(colormap) + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,12 +54,19 @@ import org.springframework.core.io.DefaultResourceLoader;
|
||||
public class SemanticSegmentation implements AutoCloseable {
|
||||
|
||||
private static final long CHANNELS = 3;
|
||||
|
||||
private static final float REQUIRED_INPUT_IMAGE_SIZE = 513f;
|
||||
|
||||
private final GraphRunner imageNormalization;
|
||||
|
||||
private final GraphRunner semanticSegmentation;
|
||||
|
||||
private final GraphRunner maskImageEncoding;
|
||||
|
||||
private final GraphRunner alphaBlending;
|
||||
|
||||
private final Tensor<Integer> colorMapTensor;
|
||||
|
||||
private final Tensor<Float> maskTransparencyTensor;
|
||||
|
||||
@Override
|
||||
@@ -75,95 +82,134 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
|
||||
public SemanticSegmentation(String modelUrl, int[][] colorMap, long[] labelFilter, float maskTransparency) {
|
||||
|
||||
this.imageNormalization = new GraphRunner("input_image", "resized_image")
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<String> input = tf.withName("input_image").placeholder(String.class);
|
||||
ExtractJpegShape<Integer> imageShapeAndChannel = tf.image.extractJpegShape(input);
|
||||
Gather<Integer> imageShape = tf.gather(imageShapeAndChannel, tf.constant(new int[] { 0, 1 }), tf.constant(0));
|
||||
this.imageNormalization = new GraphRunner("input_image", "resized_image").withGraphDefinition(tf -> {
|
||||
Placeholder<String> input = tf.withName("input_image").placeholder(String.class);
|
||||
ExtractJpegShape<Integer> imageShapeAndChannel = tf.image.extractJpegShape(input);
|
||||
Gather<Integer> imageShape = tf.gather(imageShapeAndChannel, tf.constant(new int[] { 0, 1 }),
|
||||
tf.constant(0));
|
||||
|
||||
Cast<Float> maxSize = tf.dtypes.cast(tf.max(imageShape, tf.constant(0)), Float.class);
|
||||
Div<Float> scale = tf.math.div(tf.constant(REQUIRED_INPUT_IMAGE_SIZE), maxSize);
|
||||
Cast<Integer> newSize = tf.dtypes.cast(tf.math.mul(scale, tf.dtypes.cast(imageShape, Float.class)), Integer.class);
|
||||
Cast<Float> maxSize = tf.dtypes.cast(tf.max(imageShape, tf.constant(0)), Float.class);
|
||||
Div<Float> scale = tf.math.div(tf.constant(REQUIRED_INPUT_IMAGE_SIZE), maxSize);
|
||||
Cast<Integer> newSize = tf.dtypes.cast(tf.math.mul(scale, tf.dtypes.cast(imageShape, Float.class)),
|
||||
Integer.class);
|
||||
|
||||
final Operand<Float> decodedImage =
|
||||
tf.dtypes.cast(tf.image.decodeJpeg(input, DecodeJpeg.channels(CHANNELS)), Float.class);
|
||||
final Operand<Float> decodedImage = tf.dtypes
|
||||
.cast(tf.image.decodeJpeg(input, DecodeJpeg.channels(CHANNELS)), Float.class);
|
||||
|
||||
final Operand<Float> resizedImageFloat =
|
||||
tf.image.resizeBilinear(tf.expandDims(decodedImage, tf.constant(0)), newSize);
|
||||
final Operand<Float> resizedImageFloat = tf.image
|
||||
.resizeBilinear(tf.expandDims(decodedImage, tf.constant(0)), newSize);
|
||||
|
||||
tf.withName("resized_image").dtypes.cast(resizedImageFloat, UInt8.class);
|
||||
});
|
||||
tf.withName("resized_image").dtypes.cast(resizedImageFloat, UInt8.class);
|
||||
});
|
||||
|
||||
this.semanticSegmentation = new GraphRunner("ImageTensor:0", "SemanticPredictions:0")
|
||||
.withGraphDefinition(new ProtoBufGraphDefinition(new DefaultResourceLoader().getResource(modelUrl), true));
|
||||
.withGraphDefinition(new ProtoBufGraphDefinition(new DefaultResourceLoader().getResource(modelUrl), true));
|
||||
|
||||
this.colorMapTensor = Tensor.create(colorMap).expect(Integer.class);
|
||||
|
||||
this.maskImageEncoding = new GraphRunner(Arrays.asList("color_map", "mask_pixels"), Arrays.asList("mask_png", "mask_rgb"))
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<Integer> colorTable = tf.withName("color_map").placeholder(Integer.class);
|
||||
this.maskImageEncoding = new GraphRunner(Arrays.asList("color_map", "mask_pixels"),
|
||||
Arrays.asList("mask_png", "mask_rgb"))
|
||||
.withGraphDefinition(tf -> {
|
||||
Placeholder<Integer> colorTable = tf.withName("color_map").placeholder(Integer.class);
|
||||
|
||||
Placeholder<Long> batchedMask = tf.withName("mask_pixels").placeholder(Long.class);
|
||||
// Remove batch dimension
|
||||
Squeeze<Long> mask = tf.squeeze(batchedMask, Squeeze.axis(Arrays.asList(0L)));
|
||||
Placeholder<Long> batchedMask = tf.withName("mask_pixels").placeholder(Long.class);
|
||||
// Remove batch dimension
|
||||
Squeeze<Long> mask = tf.squeeze(batchedMask, Squeeze.axis(Arrays.asList(0L)));
|
||||
|
||||
Operand<Long> filteredMask = labelFilter(tf, mask, labelFilter);
|
||||
Operand<Long> filteredMask = labelFilter(tf, mask, labelFilter);
|
||||
|
||||
// The mask can contain label values larger than the list of colors provided in the color map.
|
||||
// To avoid out-of-index errors we will "normalize" the label values in the mask to MOD max-color-table-value.
|
||||
Operand<Long> mask3 = NativeImageUtils.normalizeMaskLabels(tf, colorTable, filteredMask);
|
||||
// The mask can contain label values larger than the list of colors
|
||||
// provided in the color map.
|
||||
// To avoid out-of-index errors we will "normalize" the label values in
|
||||
// the mask to MOD max-color-table-value.
|
||||
Operand<Long> mask3 = NativeImageUtils.normalizeMaskLabels(tf, colorTable, filteredMask);
|
||||
|
||||
Gather<Integer> maskRgb = tf.withName("mask_rgb").gather(colorTable, mask3, tf.constant(0));
|
||||
Gather<Integer> maskRgb = tf.withName("mask_rgb").gather(colorTable, mask3, tf.constant(0));
|
||||
|
||||
Operand<String> png = tf.withName("mask_png").image.encodePng(tf.dtypes.cast(maskRgb, UInt8.class));
|
||||
Operand<String> png = tf.withName("mask_png").image.encodePng(tf.dtypes.cast(maskRgb, UInt8.class));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
this.maskTransparencyTensor = Tensor.create(maskTransparency).expect(Float.class);
|
||||
|
||||
this.alphaBlending = new GraphRunner(
|
||||
Arrays.asList("input_image", "mask_image", "mask_transparency"), Arrays.asList("blended_png"))
|
||||
.withGraphDefinition(tf -> {
|
||||
// Input image [B, H, W, 3]
|
||||
Cast<Float> inputImageRgb = tf.dtypes.cast(tf.withName("input_image").placeholder(UInt8.class), Float.class);
|
||||
this.alphaBlending = new GraphRunner(Arrays.asList("input_image", "mask_image", "mask_transparency"),
|
||||
Arrays.asList("blended_png"))
|
||||
.withGraphDefinition(tf -> {
|
||||
// Input image [B, H, W, 3]
|
||||
Cast<Float> inputImageRgb = tf.dtypes.cast(tf.withName("input_image").placeholder(UInt8.class),
|
||||
Float.class);
|
||||
|
||||
Placeholder<Integer> a = tf.withName("mask_image").placeholder(Integer.class);
|
||||
Cast<Float> maskRgb = tf.dtypes.cast(a, Float.class);
|
||||
Placeholder<Integer> a = tf.withName("mask_image").placeholder(Integer.class);
|
||||
Cast<Float> maskRgb = tf.dtypes.cast(a, Float.class);
|
||||
|
||||
Squeeze<Float> inputImageRgb2 = tf.squeeze(inputImageRgb, Squeeze.axis(Arrays.asList(0L)));
|
||||
Squeeze<Float> inputImageRgb2 = tf.squeeze(inputImageRgb, Squeeze.axis(Arrays.asList(0L)));
|
||||
|
||||
Placeholder<Float> maskTransparencyHolder = tf.withName("mask_transparency").placeholder(Float.class);
|
||||
Placeholder<Float> maskTransparencyHolder = tf.withName("mask_transparency").placeholder(Float.class);
|
||||
|
||||
// Blend the transparent maskImage on top of the input image.
|
||||
Operand<Float> blended = NativeImageUtils.alphaBlending(tf, maskRgb, inputImageRgb2, maskTransparencyHolder);
|
||||
// Blend the transparent maskImage on top of the input image.
|
||||
Operand<Float> blended = NativeImageUtils.alphaBlending(tf, maskRgb, inputImageRgb2,
|
||||
maskTransparencyHolder);
|
||||
|
||||
// Cut
|
||||
//Operand<Boolean> condition = tf.math.equal(a, tf.zerosLike(a));
|
||||
//Operand<Float> blended = tf.where3(condition, tf.zerosLike(maskRgb), inputImageRgb2);
|
||||
// Cut
|
||||
// Operand<Boolean> condition = tf.math.equal(a, tf.zerosLike(a));
|
||||
// Operand<Float> blended = tf.where3(condition, tf.zerosLike(maskRgb),
|
||||
// inputImageRgb2);
|
||||
|
||||
// Encode PNG
|
||||
tf.withName("blended_png").image.encodePng(tf.dtypes.cast(blended, UInt8.class));
|
||||
// Encode PNG
|
||||
tf.withName("blended_png").image.encodePng(tf.dtypes.cast(blended, UInt8.class));
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public byte[] blendMask(byte[] image) {
|
||||
try (Tensor inputTensor = Tensor.create(image); GraphRunnerMemory memory = new GraphRunnerMemory()) {
|
||||
|
||||
Map<String, Tensor<?>> blendedTensors =
|
||||
this.imageNormalization.andThen(memory) // (input_image) -> (resized_image) and memorize (resized_image)
|
||||
.andThen(this.semanticSegmentation).andThen(memory) // (ImageTensor:0) -> (SemanticPredictions:0) and memorize (SemanticPredictions:0)
|
||||
.andThen(Functions.rename("SemanticPredictions:0", "mask_pixels")) // (SemanticPredictions:0) -> (mask_pixels)
|
||||
.andThen(Functions.enrichWith("color_map", this.colorMapTensor)) // (mask_pixels) -> (mask_pixels, color_map)
|
||||
.andThen(this.maskImageEncoding).andThen(memory) // (color_map, mask_pixels) -> (mask_png, mask_rgb) and memorize (mask_png, mask_rgb)
|
||||
.andThen(Functions.enrichFromMemory(
|
||||
memory, "resized_image")) // (mask_png, mask_rgb) -> (mask_png, mask_rgb, resized_image), e.g. join the normalizedImageTensor
|
||||
.andThen(Functions.rename(
|
||||
"resized_image", "input_image",
|
||||
"mask_rgb", "mask_image")) // (mask_png, mask_rgb, resized_image) -> (mask_image, input_image)
|
||||
.andThen(Functions.enrichWith("mask_transparency", this.maskTransparencyTensor)) // (mask_image, input_image) -> (mask_image, input_image, mask_transparency)
|
||||
.andThen(this.alphaBlending).andThen(memory) // (mask_image, input_image, mask_transparency) -> (blended_png)
|
||||
.apply(Collections.singletonMap("input_image", inputTensor)); // () -> (input_image)
|
||||
Map<String, Tensor<?>> blendedTensors = this.imageNormalization.andThen(memory) // (input_image)
|
||||
// ->
|
||||
// (resized_image)
|
||||
// and
|
||||
// memorize
|
||||
// (resized_image)
|
||||
.andThen(this.semanticSegmentation)
|
||||
.andThen(memory) // (ImageTensor:0) -> (SemanticPredictions:0) and
|
||||
// memorize (SemanticPredictions:0)
|
||||
.andThen(Functions.rename("SemanticPredictions:0", "mask_pixels")) // (SemanticPredictions:0)
|
||||
// ->
|
||||
// (mask_pixels)
|
||||
.andThen(Functions.enrichWith("color_map", this.colorMapTensor)) // (mask_pixels)
|
||||
// ->
|
||||
// (mask_pixels,
|
||||
// color_map)
|
||||
.andThen(this.maskImageEncoding)
|
||||
.andThen(memory) // (color_map, mask_pixels) -> (mask_png, mask_rgb) and
|
||||
// memorize (mask_png, mask_rgb)
|
||||
.andThen(Functions.enrichFromMemory(memory, "resized_image")) // (mask_png,
|
||||
// mask_rgb)
|
||||
// ->
|
||||
// (mask_png,
|
||||
// mask_rgb,
|
||||
// resized_image),
|
||||
// e.g.
|
||||
// join
|
||||
// the
|
||||
// normalizedImageTensor
|
||||
.andThen(Functions.rename("resized_image", "input_image", "mask_rgb", "mask_image")) // (mask_png,
|
||||
// mask_rgb,
|
||||
// resized_image)
|
||||
// ->
|
||||
// (mask_image,
|
||||
// input_image)
|
||||
.andThen(Functions.enrichWith("mask_transparency", this.maskTransparencyTensor)) // (mask_image,
|
||||
// input_image)
|
||||
// ->
|
||||
// (mask_image,
|
||||
// input_image,
|
||||
// mask_transparency)
|
||||
.andThen(this.alphaBlending)
|
||||
.andThen(memory) // (mask_image, input_image, mask_transparency) ->
|
||||
// (blended_png)
|
||||
.apply(Collections.singletonMap("input_image", inputTensor)); // () ->
|
||||
// (input_image)
|
||||
|
||||
byte[] blendedImage = blendedTensors.get("blended_png").bytesValue();
|
||||
|
||||
@@ -176,15 +222,21 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
public long[][] maskPixels(byte[] image) {
|
||||
try (Tensor inputTensor = Tensor.create(image); GraphRunnerMemory memory = new GraphRunnerMemory()) {
|
||||
|
||||
return this.imageNormalization.andThen(memory) // (input_image) -> (resized_image) and memorize (resized_image)
|
||||
.andThen(this.semanticSegmentation).andThen(memory) // (ImageTensor:0) -> (SemanticPredictions:0) and memorize (SemanticPredictions:0)
|
||||
.andThen(tensorMap -> {
|
||||
Tensor<?> maskTensor = tensorMap.get("SemanticPredictions:0");
|
||||
int width = (int) maskTensor.shape()[1];
|
||||
int height = (int) maskTensor.shape()[2];
|
||||
return maskTensor.copyTo(new long[1][width][height])[0]; // 1 == batch size
|
||||
})
|
||||
.apply(Collections.singletonMap("input_image", inputTensor)); // () -> (input_image)
|
||||
return this.imageNormalization.andThen(memory) // (input_image) ->
|
||||
// (resized_image) and
|
||||
// memorize (resized_image)
|
||||
.andThen(this.semanticSegmentation)
|
||||
.andThen(memory) // (ImageTensor:0) -> (SemanticPredictions:0) and
|
||||
// memorize (SemanticPredictions:0)
|
||||
.andThen(tensorMap -> {
|
||||
Tensor<?> maskTensor = tensorMap.get("SemanticPredictions:0");
|
||||
int width = (int) maskTensor.shape()[1];
|
||||
int height = (int) maskTensor.shape()[2];
|
||||
return maskTensor.copyTo(new long[1][width][height])[0]; // 1 == batch
|
||||
// size
|
||||
})
|
||||
.apply(Collections.singletonMap("input_image", inputTensor)); // () ->
|
||||
// (input_image)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,13 +244,25 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
|
||||
try (Tensor inputTensor = Tensor.create(image); GraphRunnerMemory memory = new GraphRunnerMemory()) {
|
||||
|
||||
return this.imageNormalization.andThen(memory) // (input_image) -> (resized_image) and memorize (resized_image)
|
||||
.andThen(this.semanticSegmentation).andThen(memory) // (ImageTensor:0) -> (SemanticPredictions:0) and memorize (SemanticPredictions:0)
|
||||
.andThen(Functions.rename("SemanticPredictions:0", "mask_pixels")) // (SemanticPredictions:0) -> (mask_pixels)
|
||||
.andThen(Functions.enrichWith("color_map", this.colorMapTensor)) // (mask_pixels) -> (mask_pixels, color_map)
|
||||
.andThen(this.maskImageEncoding).andThen(memory) // (color_map, mask_pixels) -> (mask_png, mask_rgb) and memorize (mask_png, mask_rgb)
|
||||
.andThen(tensorMap -> tensorMap.get("mask_png").bytesValue())
|
||||
.apply(Collections.singletonMap("input_image", inputTensor)); // () -> (input_image)
|
||||
return this.imageNormalization.andThen(memory) // (input_image) ->
|
||||
// (resized_image) and
|
||||
// memorize (resized_image)
|
||||
.andThen(this.semanticSegmentation)
|
||||
.andThen(memory) // (ImageTensor:0) -> (SemanticPredictions:0) and
|
||||
// memorize (SemanticPredictions:0)
|
||||
.andThen(Functions.rename("SemanticPredictions:0", "mask_pixels")) // (SemanticPredictions:0)
|
||||
// ->
|
||||
// (mask_pixels)
|
||||
.andThen(Functions.enrichWith("color_map", this.colorMapTensor)) // (mask_pixels)
|
||||
// ->
|
||||
// (mask_pixels,
|
||||
// color_map)
|
||||
.andThen(this.maskImageEncoding)
|
||||
.andThen(memory) // (color_map, mask_pixels) -> (mask_png, mask_rgb) and
|
||||
// memorize (mask_png, mask_rgb)
|
||||
.andThen(tensorMap -> tensorMap.get("mask_png").bytesValue())
|
||||
.apply(Collections.singletonMap("input_image", inputTensor)); // () ->
|
||||
// (input_image)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,8 +286,7 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
|
||||
try (SemanticSegmentation segmentationService = new SemanticSegmentation(
|
||||
"https://download.tensorflow.org/models/deeplabv3_mnv2_cityscapes_train_2018_02_05.tar.gz#frozen_inference_graph.pb",
|
||||
SegmentationColorMap.loadColorMap("classpath:/colormap/citymap_colormap.json"), null, 0.45f)
|
||||
) {
|
||||
SegmentationColorMap.loadColorMap("classpath:/colormap/citymap_colormap.json"), null, 0.45f)) {
|
||||
byte[] inputImage = GraphicsUtils.loadAsByteArray("classpath:/images/amsterdam-cityscape1.jpg");
|
||||
|
||||
// 1. Mask pixels
|
||||
@@ -244,8 +307,7 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
|
||||
try (SemanticSegmentation segmentationService = new SemanticSegmentation(
|
||||
"https://download.tensorflow.org/models/deeplabv3_xception_ade20k_train_2018_05_29.tar.gz#frozen_inference_graph.pb",
|
||||
SegmentationColorMap.loadColorMap("classpath:/colormap/ade20k_colormap.json"), null, 0.45f)
|
||||
) {
|
||||
SegmentationColorMap.loadColorMap("classpath:/colormap/ade20k_colormap.json"), null, 0.45f)) {
|
||||
byte[] inputImage = GraphicsUtils.loadAsByteArray("classpath:/images/interior.jpg");
|
||||
|
||||
// 1. Mask pixels
|
||||
@@ -264,8 +326,7 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
|
||||
try (SemanticSegmentation segmentationService = new SemanticSegmentation(
|
||||
"https://download.tensorflow.org/models/deeplabv3_mnv2_pascal_trainval_2018_01_29.tar.gz#frozen_inference_graph.pb",
|
||||
SegmentationColorMap.loadColorMap("classpath:/colormap/black_white_colormap.json"), null, 0.45f)
|
||||
) {
|
||||
SegmentationColorMap.loadColorMap("classpath:/colormap/black_white_colormap.json"), null, 0.45f)) {
|
||||
byte[] inputImage = GraphicsUtils.loadAsByteArray("classpath:/images/VikiMaxiAdi.jpg");
|
||||
|
||||
// 1. Mask pixels
|
||||
@@ -283,4 +344,5 @@ public class SemanticSegmentation implements AutoCloseable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
|
||||
|
||||
/**
|
||||
*
|
||||
* Semantic image segmentation - the task of assigning a semantic label, such as "road", "sky", "person", "dog", to
|
||||
* every pixel in an image.
|
||||
* Semantic image segmentation - the task of assigning a semantic label, such as "road",
|
||||
* "sky", "person", "dog", to every pixel in an image.
|
||||
*
|
||||
* https://ai.googleblog.com/2018/03/semantic-image-segmentation-with.html
|
||||
* https://github.com/tensorflow/models/blob/master/research/deeplab/g3doc/model_zoo.md
|
||||
@@ -65,11 +65,14 @@ public class SemanticSegmentationUtils {
|
||||
|
||||
/** INPUT_TENSOR_NAME . */
|
||||
public static final String INPUT_TENSOR_NAME = "ImageTensor:0";
|
||||
|
||||
/** OUTPUT_TENSOR_NAME . */
|
||||
public static final String OUTPUT_TENSOR_NAME = "SemanticPredictions:0";
|
||||
|
||||
private static final int BATCH_SIZE = 1;
|
||||
|
||||
private static final long CHANNELS = 3;
|
||||
|
||||
private static final int REQUIRED_INPUT_IMAGE_SIZE = 513;
|
||||
|
||||
public static BufferedImage scaledImage(String imagePath) {
|
||||
@@ -100,9 +103,11 @@ public class SemanticSegmentationUtils {
|
||||
int newHeight = (int) (originalImage.getHeight() * scale);
|
||||
|
||||
Image tmpImage = originalImage.getScaledInstance(newWidth, newHeight, Image.SCALE_DEFAULT);
|
||||
//BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, TYPE_INT_BGR);
|
||||
// BufferedImage resizedImage = new BufferedImage(newWidth, newHeight,
|
||||
// TYPE_INT_BGR);
|
||||
BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, TYPE_3BYTE_BGR);
|
||||
//BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, originalImage.getType());
|
||||
// BufferedImage resizedImage = new BufferedImage(newWidth, newHeight,
|
||||
// originalImage.getType());
|
||||
|
||||
Graphics2D g2d = resizedImage.createGraphics();
|
||||
g2d.drawImage(tmpImage, 0, 0, null);
|
||||
@@ -125,7 +130,8 @@ public class SemanticSegmentationUtils {
|
||||
// ImageIO.read produces BGR-encoded images, while the model expects RGB.
|
||||
byte[] data = bgrToRgb(toBytes(scaledImage));
|
||||
|
||||
// Expand dimensions since the model expects images to have shape: [1, None, None, 3]
|
||||
// Expand dimensions since the model expects images to have shape: [1, None, None,
|
||||
// 3]
|
||||
long[] shape = new long[] { BATCH_SIZE, scaledImage.getHeight(), scaledImage.getWidth(), CHANNELS };
|
||||
|
||||
return Tensor.create(UInt8.class, shape, ByteBuffer.wrap(data));
|
||||
@@ -201,7 +207,8 @@ public class SemanticSegmentationUtils {
|
||||
|
||||
public String serializeToJson(int[][] pixels) {
|
||||
String masksBase64 = Base64.getEncoder().encodeToString(toBytes(pixels));
|
||||
return String.format("{ \"columns\":%d, \"rows\":%d, \"masks\":\"%s\"}", pixels.length, pixels[0].length, masksBase64);
|
||||
return String.format("{ \"columns\":%d, \"rows\":%d, \"masks\":\"%s\"}", pixels.length, pixels[0].length,
|
||||
masksBase64);
|
||||
}
|
||||
|
||||
public int[][] deserializeToMasks(String json) throws IOException {
|
||||
@@ -221,7 +228,7 @@ public class SemanticSegmentationUtils {
|
||||
b[bi + 0] = (byte) (i >> 24);
|
||||
b[bi + 1] = (byte) (i >> 16);
|
||||
b[bi + 2] = (byte) (i >> 8);
|
||||
b[bi + 3] = (byte) (i /*>> 0*/);
|
||||
b[bi + 3] = (byte) (i /* >> 0 */);
|
||||
bi = bi + 4;
|
||||
}
|
||||
}
|
||||
@@ -233,10 +240,7 @@ public class SemanticSegmentationUtils {
|
||||
int bi = 0;
|
||||
for (int i = 0; i < ic; i++) {
|
||||
for (int j = 0; j < jc; j++) {
|
||||
intResult[i][j] = (b[bi] << 24) +
|
||||
(b[bi + 1] << 16) +
|
||||
(b[bi + 2] << 8) +
|
||||
b[bi + 3];
|
||||
intResult[i][j] = (b[bi] << 24) + (b[bi + 1] << 16) + (b[bi + 2] << 8) + b[bi + 3];
|
||||
bi = bi + 4;
|
||||
}
|
||||
}
|
||||
@@ -246,14 +250,16 @@ public class SemanticSegmentationUtils {
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
// PASCAL VOC 2012
|
||||
//String tensorflowModelLocation = "file:/Users/ctzolov/Downloads/deeplabv3_mnv2_pascal_train_aug/frozen_inference_graph.pb";
|
||||
//String imagePath = "classpath:/images/VikiMaxiAdi.jpg";
|
||||
// String tensorflowModelLocation =
|
||||
// "file:/Users/ctzolov/Downloads/deeplabv3_mnv2_pascal_train_aug/frozen_inference_graph.pb";
|
||||
// String imagePath = "classpath:/images/VikiMaxiAdi.jpg";
|
||||
|
||||
// CITYSCAPE
|
||||
//String tensorflowModelLocation = "file:/Users/ctzolov/Downloads/deeplabv3_mnv2_cityscapes_train/frozen_inference_graph.pb";
|
||||
//String imagePath = "classpath:/images/amsterdam-cityscape1.jpg";
|
||||
//String imagePath = "classpath:/images/amsterdam-channel.jpg";
|
||||
//String imagePath = "classpath:/images/landsmeer.png";
|
||||
// String tensorflowModelLocation =
|
||||
// "file:/Users/ctzolov/Downloads/deeplabv3_mnv2_cityscapes_train/frozen_inference_graph.pb";
|
||||
// String imagePath = "classpath:/images/amsterdam-cityscape1.jpg";
|
||||
// String imagePath = "classpath:/images/amsterdam-channel.jpg";
|
||||
// String imagePath = "classpath:/images/landsmeer.png";
|
||||
|
||||
// ADE20K
|
||||
String tensorflowModelLocation = "file:/Users/ctzolov/Downloads/deeplabv3_xception_ade20k_train/frozen_inference_graph.pb";
|
||||
@@ -261,7 +267,8 @@ public class SemanticSegmentationUtils {
|
||||
|
||||
BufferedImage inputImage = ImageIO.read(new DefaultResourceLoader().getResource(imagePath).getInputStream());
|
||||
|
||||
TensorFlowService tf = new TensorFlowService(new DefaultResourceLoader().getResource(tensorflowModelLocation), Arrays.asList(OUTPUT_TENSOR_NAME));
|
||||
TensorFlowService tf = new TensorFlowService(new DefaultResourceLoader().getResource(tensorflowModelLocation),
|
||||
Arrays.asList(OUTPUT_TENSOR_NAME));
|
||||
|
||||
SemanticSegmentationUtils segmentationService = new SemanticSegmentationUtils();
|
||||
|
||||
@@ -275,15 +282,24 @@ public class SemanticSegmentationUtils {
|
||||
|
||||
int height = (int) maskPixelsTensor.shape()[1];
|
||||
int width = (int) maskPixelsTensor.shape()[2];
|
||||
long[][] maskPixels = maskPixelsTensor.copyTo(new long[BATCH_SIZE][height][width])[0]; // take 0 because the batch size is 1.
|
||||
long[][] maskPixels = maskPixelsTensor.copyTo(new long[BATCH_SIZE][height][width])[0]; // take
|
||||
// 0
|
||||
// because
|
||||
// the
|
||||
// batch
|
||||
// size
|
||||
// is
|
||||
// 1.
|
||||
|
||||
int[][] maskPixelsInt = segmentationService.toIntArray(maskPixels);
|
||||
|
||||
BufferedImage maskImage = segmentationService.createMaskImage(maskPixelsInt, scaledImage.getWidth(), scaledImage.getHeight(), 0.35);
|
||||
BufferedImage maskImage = segmentationService.createMaskImage(maskPixelsInt, scaledImage.getWidth(),
|
||||
scaledImage.getHeight(), 0.35);
|
||||
|
||||
BufferedImage blended = segmentationService.blendMask(maskImage, scaledImage);
|
||||
|
||||
ImageIO.write(maskImage, "png", new File("./semantic-segmentation/target/java2Dmask.jpg"));
|
||||
ImageIO.write(blended, "png", new File("./semantic-segmentation/target/java2Dblended.jpg"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ public class SpelFunctionConfiguration {
|
||||
public ExpressionEvaluatingTransformer expressionEvaluatingTransformer(
|
||||
SpelFunctionProperties spelFunctionProperties) {
|
||||
|
||||
return new ExpressionEvaluatingTransformer(new SpelExpressionParser()
|
||||
.parseExpression(spelFunctionProperties.getExpression()));
|
||||
return new ExpressionEvaluatingTransformer(
|
||||
new SpelExpressionParser().parseExpression(spelFunctionProperties.getExpression()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ public class SpelFunctionApplicationTests {
|
||||
@Test
|
||||
public void testJson() {
|
||||
Message<?> message = MessageBuilder.withPayload("{\"foo\":\"bar\"}")
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON).build();
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
|
||||
.build();
|
||||
final Message<?> transformed = this.transformer.apply(message);
|
||||
assertThat(transformed.getPayload()).isEqualTo("{\"FOO\":\"BAR\"}");
|
||||
}
|
||||
@@ -57,4 +58,5 @@ public class SpelFunctionApplicationTests {
|
||||
static class SpelFunctionTestApplication {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ public class SplitterFunctionConfiguration {
|
||||
@ConditionalOnProperty(prefix = "splitter", name = "expression")
|
||||
public AbstractMessageSplitter expressionSplitter(SplitterFunctionProperties splitterFunctionProperties) {
|
||||
return new ExpressionEvaluatingSplitter(
|
||||
new SpelExpressionParser()
|
||||
.parseExpression(splitterFunctionProperties.getExpression()));
|
||||
new SpelExpressionParser().parseExpression(splitterFunctionProperties.getExpression()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -96,10 +95,12 @@ public class SplitterFunctionConfiguration {
|
||||
|
||||
@ConditionalOnProperty(prefix = "splitter", name = "charset")
|
||||
static class Charset {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(prefix = "splitter", name = "fileMarkers")
|
||||
static class FileMarkers {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,33 +37,29 @@ public class SplitterFunctionProperties {
|
||||
private String expression;
|
||||
|
||||
/**
|
||||
* When expression is null, delimiters to use when tokenizing
|
||||
* {@link String} payloads.
|
||||
* When expression is null, delimiters to use when tokenizing {@link String} payloads.
|
||||
*/
|
||||
private String delimiters;
|
||||
|
||||
/**
|
||||
* Set to true or false to use a {@code FileSplitter} (to split
|
||||
* text-based files by line) that includes
|
||||
* (or not) beginning/end of file markers.
|
||||
* Set to true or false to use a {@code FileSplitter} (to split text-based files by
|
||||
* line) that includes (or not) beginning/end of file markers.
|
||||
*/
|
||||
private Boolean fileMarkers;
|
||||
|
||||
/**
|
||||
* When 'fileMarkers == true', specify if they should be produced
|
||||
* as FileSplitter.FileMarker objects or JSON.
|
||||
* When 'fileMarkers == true', specify if they should be produced as
|
||||
* FileSplitter.FileMarker objects or JSON.
|
||||
*/
|
||||
private boolean markersJson = true;
|
||||
|
||||
/**
|
||||
* The charset to use when converting bytes in text-based files
|
||||
* to String.
|
||||
* The charset to use when converting bytes in text-based files to String.
|
||||
*/
|
||||
private String charset;
|
||||
|
||||
/**
|
||||
* Add correlation/sequence information in headers to facilitate later
|
||||
* aggregation.
|
||||
* Add correlation/sequence information in headers to facilitate later aggregation.
|
||||
*/
|
||||
private boolean applySequence = true;
|
||||
|
||||
@@ -122,7 +118,8 @@ public class SplitterFunctionProperties {
|
||||
|
||||
@AssertTrue(message = "File properties are not allowed when an 'expression' or 'delimiters' property is provided")
|
||||
public boolean isFilePropsAllowed() {
|
||||
return !(this.expression != null || this.delimiters != null) || this.fileMarkers == null && this.charset == null;
|
||||
return !(this.expression != null || this.delimiters != null)
|
||||
|| this.fileMarkers == null && this.charset == null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,5 +45,7 @@ public class SplitterFunctionApplicationTests {
|
||||
|
||||
@SpringBootApplication
|
||||
static class SplitterFunctionTestApplication {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ import java.util.Collection;
|
||||
import org.springframework.integration.handler.MessageProcessor;
|
||||
|
||||
public interface CommandLineArgumentsMessageMapper extends MessageProcessor<Collection<String>> {
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import java.util.Map;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Parses a comma delimited list of key value pairs in which the values can contain commas as well.
|
||||
* Parses a comma delimited list of key value pairs in which the values can contain commas
|
||||
* as well.
|
||||
*
|
||||
* @author Chris Schaeffer
|
||||
* @author David Turanski
|
||||
@@ -63,4 +64,5 @@ abstract class KeyValueListParser {
|
||||
properties.put(pair.substring(0, firstEquals).trim(), pair.substring(firstEquals + 1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TaskLaunchRequest {
|
||||
|
||||
@JsonProperty("args")
|
||||
private List<String> commandlineArguments = new ArrayList<>();
|
||||
|
||||
@@ -62,4 +63,5 @@ public class TaskLaunchRequest {
|
||||
this.commandlineArguments.addAll(args);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,13 +36,17 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Configuration for a {@link TaskLaunchRequestFunction}, provided as a common function that can be composed with other Suppliers or
|
||||
* Functions to transform any {@link Message} to a {@link TaskLaunchRequest} which may be used as input to the {@code TaskLauncherFunction} to launch a task.
|
||||
* Configuration for a {@link TaskLaunchRequestFunction}, provided as a common function
|
||||
* that can be composed with other Suppliers or Functions to transform any {@link Message}
|
||||
* to a {@link TaskLaunchRequest} which may be used as input to the
|
||||
* {@code TaskLauncherFunction} to launch a task.
|
||||
*
|
||||
* Command line arguments used by the task, as well as the task name itself may be statically configured or extracted from
|
||||
* the message contents, using SpEL. See {@link TaskLaunchRequestFunctionProperties} for details.
|
||||
* Command line arguments used by the task, as well as the task name itself may be
|
||||
* statically configured or extracted from the message contents, using SpEL. See
|
||||
* {@link TaskLaunchRequestFunctionProperties} for details.
|
||||
*
|
||||
* It is also possible to provide your own implementations of {@link CommandLineArgumentsMessageMapper} and {@link TaskNameMessageMapper}.
|
||||
* It is also possible to provide your own implementations of
|
||||
* {@link CommandLineArgumentsMessageMapper} and {@link TaskNameMessageMapper}.
|
||||
*
|
||||
* @author David Turanski
|
||||
**/
|
||||
@@ -58,9 +62,8 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
/**
|
||||
* A {@link java.util.function.Function} to transform a {@link Message} payload to a
|
||||
* {@link TaskLaunchRequest}.
|
||||
*
|
||||
* @param taskLaunchRequestMessageProcessor a {@link TaskLaunchRequestMessageProcessor}.
|
||||
*
|
||||
* @param taskLaunchRequestMessageProcessor a
|
||||
* {@link TaskLaunchRequestMessageProcessor}.
|
||||
* @return a {@code TaskLaunchRequest} Message.
|
||||
*/
|
||||
@Bean(name = TASK_LAUNCH_REQUEST_FUNCTION_NAME)
|
||||
@@ -78,10 +81,8 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||
@Bean
|
||||
public TaskLaunchRequestMessageProcessor taskLaunchRequestMessageProcessor(
|
||||
TaskLaunchRequestSupplier taskLaunchRequestInitializer,
|
||||
TaskLaunchRequestFunctionProperties properties,
|
||||
EvaluationContext evaluationContext,
|
||||
@Nullable TaskNameMessageMapper taskNameMessageMapper,
|
||||
TaskLaunchRequestSupplier taskLaunchRequestInitializer, TaskLaunchRequestFunctionProperties properties,
|
||||
EvaluationContext evaluationContext, @Nullable TaskNameMessageMapper taskNameMessageMapper,
|
||||
@Nullable CommandLineArgumentsMessageMapper commandLineArgumentsMessageMapper) {
|
||||
|
||||
if (taskNameMessageMapper == null) {
|
||||
@@ -92,8 +93,7 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
commandLineArgumentsMessageMapper = commandLineArgumentsMessageMapper(properties, evaluationContext);
|
||||
}
|
||||
|
||||
return new TaskLaunchRequestMessageProcessor(taskLaunchRequestInitializer,
|
||||
taskNameMessageMapper,
|
||||
return new TaskLaunchRequestMessageProcessor(taskLaunchRequestInitializer, taskNameMessageMapper,
|
||||
commandLineArgumentsMessageMapper);
|
||||
}
|
||||
|
||||
@@ -103,11 +103,11 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
}
|
||||
|
||||
private TaskNameMessageMapper taskNameMessageMapper(TaskLaunchRequestFunctionProperties taskLaunchRequestProperties,
|
||||
EvaluationContext evaluationContext) {
|
||||
EvaluationContext evaluationContext) {
|
||||
if (StringUtils.hasText(taskLaunchRequestProperties.getTaskNameExpression())) {
|
||||
SpelExpressionParser expressionParser = new SpelExpressionParser();
|
||||
Expression taskNameExpression = expressionParser
|
||||
.parseExpression(taskLaunchRequestProperties.getTaskNameExpression());
|
||||
.parseExpression(taskLaunchRequestProperties.getTaskNameExpression());
|
||||
return new ExpressionEvaluatingTaskNameMessageMapper(taskNameExpression, evaluationContext);
|
||||
}
|
||||
|
||||
@@ -122,23 +122,23 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
}
|
||||
|
||||
private static class TaskLaunchRequestPropertiesInitializer extends TaskLaunchRequestSupplier {
|
||||
TaskLaunchRequestPropertiesInitializer(
|
||||
TaskLaunchRequestFunctionProperties taskLaunchRequestProperties) {
|
||||
|
||||
this.commandLineArgumentSupplier(
|
||||
() -> new ArrayList<>(taskLaunchRequestProperties.getArgs()));
|
||||
TaskLaunchRequestPropertiesInitializer(TaskLaunchRequestFunctionProperties taskLaunchRequestProperties) {
|
||||
|
||||
this.deploymentPropertiesSupplier(
|
||||
() -> KeyValueListParser.parseCommaDelimitedKeyValuePairs(
|
||||
taskLaunchRequestProperties.getDeploymentProperties()));
|
||||
this.commandLineArgumentSupplier(() -> new ArrayList<>(taskLaunchRequestProperties.getArgs()));
|
||||
|
||||
this.deploymentPropertiesSupplier(() -> KeyValueListParser
|
||||
.parseCommaDelimitedKeyValuePairs(taskLaunchRequestProperties.getDeploymentProperties()));
|
||||
|
||||
this.taskNameSupplier(() -> taskLaunchRequestProperties.getTaskName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ExpressionEvaluatingTaskNameMessageMapper implements TaskNameMessageMapper {
|
||||
|
||||
private final Expression expression;
|
||||
|
||||
private final EvaluationContext evaluationContext;
|
||||
|
||||
ExpressionEvaluatingTaskNameMessageMapper(Expression expression, EvaluationContext evaluationContext) {
|
||||
@@ -150,9 +150,11 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
public String processMessage(Message<?> message) {
|
||||
return expression.getValue(evaluationContext, message).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ExpressionEvaluatingCommandLineArgsMapper implements CommandLineArgumentsMessageMapper {
|
||||
|
||||
private final Map<String, Expression> argExpressionsMap;
|
||||
|
||||
private final EvaluationContext evaluationContext;
|
||||
@@ -163,8 +165,8 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
if (StringUtils.hasText(argExpressions)) {
|
||||
SpelExpressionParser expressionParser = new SpelExpressionParser();
|
||||
|
||||
KeyValueListParser.parseCommaDelimitedKeyValuePairs(argExpressions).forEach(
|
||||
(k, v) -> argExpressionsMap.put(k, expressionParser.parseExpression(v)));
|
||||
KeyValueListParser.parseCommaDelimitedKeyValuePairs(argExpressions)
|
||||
.forEach((k, v) -> argExpressionsMap.put(k, expressionParser.parseExpression(v)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +178,10 @@ public class TaskLaunchRequestFunctionConfiguration {
|
||||
private Collection<String> evaluateArgExpressions(Message<?> message) {
|
||||
List<String> results = new LinkedList<>();
|
||||
this.argExpressionsMap.forEach((k, expression) -> results
|
||||
.add(String.format("%s=%s", k, expression.getValue(this.evaluationContext, message))));
|
||||
.add(String.format("%s=%s", k, expression.getValue(this.evaluationContext, message))));
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ public class TaskLaunchRequestFunctionProperties {
|
||||
*/
|
||||
private String taskName;
|
||||
|
||||
|
||||
/**
|
||||
* A SpEL expression to extract the task name from each Message, using the Message as the evaluation context.
|
||||
* A SpEL expression to extract the task name from each Message, using the Message as
|
||||
* the evaluation context.
|
||||
*/
|
||||
private String taskNameExpression;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class TaskLaunchRequestMessageProcessor implements MessagePostProcessor {
|
||||
taskLaunchRequest.addCommmandLineArguments(commandLineArgumentsMessageMapper.processMessage(message));
|
||||
|
||||
MessageBuilder<TaskLaunchRequest> builder = MessageBuilder.withPayload(taskLaunchRequest)
|
||||
.copyHeaders(message.getHeaders());
|
||||
.copyHeaders(message.getHeaders());
|
||||
return adjustHeaders(builder).build();
|
||||
}
|
||||
|
||||
@@ -65,4 +65,5 @@ class TaskLaunchRequestMessageProcessor implements MessagePostProcessor {
|
||||
builder.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON);
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -64,4 +64,5 @@ class TaskLaunchRequestSupplier implements Supplier<TaskLaunchRequest> {
|
||||
|
||||
return taskLaunchRequest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@ import org.springframework.integration.handler.MessageProcessor;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TaskNameMessageMapper extends MessageProcessor<String> {
|
||||
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ public class KeyValueListParserTests {
|
||||
|
||||
@Test
|
||||
public void testParseSimpleDeploymentProperty() {
|
||||
Map<String, String> deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs(
|
||||
"app.sftp.param=value");
|
||||
Map<String, String> deploymentProperties = KeyValueListParser
|
||||
.parseCommaDelimitedKeyValuePairs("app.sftp.param=value");
|
||||
assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(),
|
||||
deploymentProperties.size() == 1);
|
||||
assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.param"));
|
||||
@@ -41,8 +41,8 @@ public class KeyValueListParserTests {
|
||||
|
||||
@Test
|
||||
public void testParseSimpleDeploymentPropertyMultipleValues() {
|
||||
Map<String, String> deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs(
|
||||
"app.sftp.param=value1,value2,value3");
|
||||
Map<String, String> deploymentProperties = KeyValueListParser
|
||||
.parseCommaDelimitedKeyValuePairs("app.sftp.param=value1,value2,value3");
|
||||
|
||||
assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(),
|
||||
deploymentProperties.size() == 1);
|
||||
@@ -55,8 +55,7 @@ public class KeyValueListParserTests {
|
||||
Map<String, String> argExpressions = KeyValueListParser.parseCommaDelimitedKeyValuePairs(
|
||||
"arg1=payload.substr(0,2),arg2=headers['foo'],arg3=headers['bar']==false");
|
||||
|
||||
assertTrue("Invalid number of deployment properties: " + argExpressions.size(),
|
||||
argExpressions.size() == 3);
|
||||
assertTrue("Invalid number of deployment properties: " + argExpressions.size(), argExpressions.size() == 3);
|
||||
assertTrue("Expected deployment key not found", argExpressions.containsKey("arg1"));
|
||||
assertEquals("Invalid deployment value", "payload.substr(0,2)", argExpressions.get("arg1"));
|
||||
|
||||
@@ -69,8 +68,8 @@ public class KeyValueListParserTests {
|
||||
|
||||
@Test
|
||||
public void testParseMultipleDeploymentPropertiesSingleValue() {
|
||||
Map<String, String> deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs(
|
||||
"app.sftp.param=value1,app.sftp.other.param=value2");
|
||||
Map<String, String> deploymentProperties = KeyValueListParser
|
||||
.parseCommaDelimitedKeyValuePairs("app.sftp.param=value1,app.sftp.other.param=value2");
|
||||
|
||||
assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(),
|
||||
deploymentProperties.size() == 2);
|
||||
@@ -84,8 +83,8 @@ public class KeyValueListParserTests {
|
||||
public void testParseMultipleDeploymentPropertiesMultipleValues() {
|
||||
TaskLaunchRequestFunctionProperties taskLaunchRequestProperties = new TaskLaunchRequestFunctionProperties();
|
||||
|
||||
Map<String, String> deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs(
|
||||
"app.sftp.param=value1,value2,app.sftp.other.param=other1,other2");
|
||||
Map<String, String> deploymentProperties = KeyValueListParser
|
||||
.parseCommaDelimitedKeyValuePairs("app.sftp.param=value1,value2,app.sftp.other.param=other1,other2");
|
||||
|
||||
assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(),
|
||||
deploymentProperties.size() == 2);
|
||||
@@ -94,4 +93,5 @@ public class KeyValueListParserTests {
|
||||
assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.other.param"));
|
||||
assertEquals("Invalid deployment value", "other1,other2", deploymentProperties.get("app.sftp.other.param"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,18 +45,17 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
springApplicationBuilder = new SpringApplicationBuilder(TaskLaunchRequestFunctionTestApplication.class)
|
||||
.web(WebApplicationType.NONE);
|
||||
.web(WebApplicationType.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void simpleDataflowTaskLaunchRequest() throws IOException {
|
||||
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false",
|
||||
"spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo")
|
||||
.run();
|
||||
|
||||
TaskLaunchRequest taskLaunchRequest = verifyAndreceiveTaskLaunchRequest(context);
|
||||
|
||||
@@ -69,16 +68,15 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@DirtiesContext
|
||||
public void dataflowTaskLaunchRequestWithArgsAndDeploymentProperties() throws IOException {
|
||||
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo", "task.launch.request.args=foo=bar,baz=boo",
|
||||
"task.launch.request.deploymentProperties=count=3")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo", "task.launch.request.args=foo=bar,baz=boo",
|
||||
"task.launch.request.deploymentProperties=count=3")
|
||||
.run();
|
||||
TaskLaunchRequest taskLaunchRequest = verifyAndreceiveTaskLaunchRequest(context);
|
||||
|
||||
assertThat(taskLaunchRequest.getTaskName()).isEqualTo("foo");
|
||||
assertThat(taskLaunchRequest.getCommandlineArguments()).containsExactlyInAnyOrder("foo=bar",
|
||||
"baz=boo");
|
||||
assertThat(taskLaunchRequest.getCommandlineArguments()).containsExactlyInAnyOrder("foo=bar", "baz=boo");
|
||||
assertThat(taskLaunchRequest.getDeploymentProperties()).containsOnly(entry("count", "3"));
|
||||
}
|
||||
|
||||
@@ -86,10 +84,10 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@DirtiesContext
|
||||
public void taskLaunchRequestWithCommandLineArgsMessageMapper() throws IOException {
|
||||
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo", "enhanceTLRArgs=true")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo", "enhanceTLRArgs=true")
|
||||
.run();
|
||||
|
||||
TaskLaunchRequest taskLaunchRequest = verifyAndreceiveTaskLaunchRequest(context);
|
||||
|
||||
@@ -102,12 +100,11 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void taskLaunchRequestWithArgExpressions() throws IOException {
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false",
|
||||
"spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo",
|
||||
"task.launch.request.arg-expressions=foo=payload.toUpperCase(),bar=payload.substring(0,2)")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo",
|
||||
"task.launch.request.arg-expressions=foo=payload.toUpperCase(),bar=payload.substring(0,2)")
|
||||
.run();
|
||||
|
||||
Message<String> message = MessageBuilder.withPayload("hello").build();
|
||||
|
||||
@@ -123,11 +120,10 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void taskLaunchRequestWithIntPayload() throws IOException {
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo",
|
||||
"task.launch.request.arg-expressions=i=payload")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name=foo", "task.launch.request.arg-expressions=i=payload")
|
||||
.run();
|
||||
|
||||
TaskLaunchRequestFunction taskLaunchRequestFunction = context.getBean(TaskLaunchRequestFunction.class);
|
||||
|
||||
@@ -144,10 +140,10 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void taskNameExpression() throws IOException {
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name-expression=payload+'_task'")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"task.launch.request.task-name-expression=payload+'_task'")
|
||||
.run();
|
||||
|
||||
TaskLaunchRequestFunction taskLaunchRequestFunction = context.getBean(TaskLaunchRequestFunction.class);
|
||||
|
||||
@@ -163,10 +159,10 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void customTaskNameExtractor() throws IOException {
|
||||
ApplicationContext context = springApplicationBuilder.properties(
|
||||
"spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"customTaskNameExtractor=true")
|
||||
.run();
|
||||
ApplicationContext context = springApplicationBuilder
|
||||
.properties("spring.jmx.enabled=false", "spring.cloud.function.definition=taskLaunchRequestFunction",
|
||||
"customTaskNameExtractor=true")
|
||||
.run();
|
||||
TaskLaunchRequestFunction taskLaunchRequestFunction = context.getBean(TaskLaunchRequestFunction.class);
|
||||
|
||||
Message<String> message = MessageBuilder.withPayload("foo").build();
|
||||
@@ -184,11 +180,10 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
assertThat(request.getTaskName()).isEqualTo("defaultTask");
|
||||
}
|
||||
|
||||
private TaskLaunchRequest verifyAndreceiveTaskLaunchRequest(ApplicationContext context)
|
||||
throws IOException {
|
||||
private TaskLaunchRequest verifyAndreceiveTaskLaunchRequest(ApplicationContext context) throws IOException {
|
||||
TaskLaunchRequestFunction taskLaunchRequestFunction = context.getBean(TaskLaunchRequestFunction.class);
|
||||
Message<TaskLaunchRequest> message = taskLaunchRequestFunction
|
||||
.apply(MessageBuilder.withPayload(new byte[] {}).build());
|
||||
.apply(MessageBuilder.withPayload(new byte[] {}).build());
|
||||
assertThat(message).isNotNull();
|
||||
return message.getPayload();
|
||||
}
|
||||
@@ -207,5 +202,7 @@ public class TaskLaunchRequestFunctionApplicationTests {
|
||||
CommandLineArgumentsMessageMapper commandLineArgumentsProvider() {
|
||||
return message -> Collections.singletonList("runtimeArg");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,4 +73,5 @@ public class TaskLaunchRequestFunctionPropertiesTests {
|
||||
static class Conf {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Configuration
|
||||
@@ -103,11 +102,10 @@ public class TwitterGeoFunctionConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, Message<byte[]>> twitterGeoFunction(
|
||||
Function<Message<?>, GeoQuery> toGeoQuery,
|
||||
Function<GeoQuery, List<Place>> places,
|
||||
Function<Object, Message<byte[]>> managedJson) {
|
||||
public Function<Message<?>, Message<byte[]>> twitterGeoFunction(Function<Message<?>, GeoQuery> toGeoQuery,
|
||||
Function<GeoQuery, List<Place>> places, Function<Object, Message<byte[]>> managedJson) {
|
||||
|
||||
return toGeoQuery.andThen(places).andThen(managedJson)::apply;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@@ -35,8 +34,10 @@ public class TwitterGeoFunctionProperties {
|
||||
private static final Expression DEFAULT_EXPRESSION = new SpelExpressionParser().parseExpression("payload");
|
||||
|
||||
public enum GeoType {
|
||||
|
||||
/** Geo retrieval type. */
|
||||
reverse, search
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,22 +57,24 @@ public class TwitterGeoFunctionProperties {
|
||||
private Location location = new Location();
|
||||
|
||||
/**
|
||||
* Hints for the number of results to return. This does not guarantee that the number of results
|
||||
* returned will equal max_results, but instead informs how many 'nearby' results to return.
|
||||
* Hints for the number of results to return. This does not guarantee that the number
|
||||
* of results returned will equal max_results, but instead informs how many 'nearby'
|
||||
* results to return.
|
||||
*/
|
||||
private int maxResults = -1;
|
||||
|
||||
/**
|
||||
* Sets a hint on the 'region' in which to search. If a number, then this is a radius in meters, but it
|
||||
* can also take a string that is suffixed with ft to specify feet. If this is not passed in, then it is
|
||||
* assumed to be 0m. If coming from a device, in practice, this value is whatever accuracy the device
|
||||
* has measuring its location (whether it be coming from a GPS, WiFi triangulation, etc.).
|
||||
* Sets a hint on the 'region' in which to search. If a number, then this is a radius
|
||||
* in meters, but it can also take a string that is suffixed with ft to specify feet.
|
||||
* If this is not passed in, then it is assumed to be 0m. If coming from a device, in
|
||||
* practice, this value is whatever accuracy the device has measuring its location
|
||||
* (whether it be coming from a GPS, WiFi triangulation, etc.).
|
||||
*/
|
||||
private String accuracy = null;
|
||||
|
||||
/**
|
||||
* Minimal granularity of data to return. If this is not passed in, then neighborhood is assumed.
|
||||
* City can also be passed.
|
||||
* Minimal granularity of data to return. If this is not passed in, then neighborhood
|
||||
* is assumed. City can also be passed.
|
||||
*/
|
||||
private String granularity = null;
|
||||
|
||||
@@ -125,7 +128,8 @@ public class TwitterGeoFunctionProperties {
|
||||
|
||||
@AssertTrue(message = "Either the IP or the Location must be set")
|
||||
public boolean isAtLeastOne() {
|
||||
return this.getSearch().getIp() == null ^ (this.getLocation().getLat() == null && this.getLocation().getLon() == null);
|
||||
return this.getSearch().getIp() == null
|
||||
^ (this.getLocation().getLat() == null && this.getLocation().getLon() == null);
|
||||
}
|
||||
|
||||
@AssertTrue(message = "The IP parameter is applicable only for 'Search' GeoType")
|
||||
@@ -137,9 +141,10 @@ public class TwitterGeoFunctionProperties {
|
||||
}
|
||||
|
||||
public static class Search {
|
||||
|
||||
/**
|
||||
* An IP address. Used when attempting to fix geolocation based off of the user's IP address.
|
||||
* Applicable only for 'search' geo type.
|
||||
* An IP address. Used when attempting to fix geolocation based off of the user's
|
||||
* IP address. Applicable only for 'search' geo type.
|
||||
*/
|
||||
private Expression ip = null;
|
||||
|
||||
@@ -163,6 +168,7 @@ public class TwitterGeoFunctionProperties {
|
||||
public void setQuery(Expression query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Location {
|
||||
@@ -177,7 +183,6 @@ public class TwitterGeoFunctionProperties {
|
||||
*/
|
||||
private Expression lon;
|
||||
|
||||
|
||||
public Expression getLat() {
|
||||
return lat;
|
||||
}
|
||||
@@ -193,5 +198,7 @@ public class TwitterGeoFunctionProperties {
|
||||
public void setLon(Expression lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ public class TwitterTrendFunctionConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, List<Location>> closestOrAvailableTrends(
|
||||
TwitterTrendFunctionProperties properties, Twitter twitter) {
|
||||
public Function<Message<?>, List<Location>> closestOrAvailableTrends(TwitterTrendFunctionProperties properties,
|
||||
Twitter twitter) {
|
||||
return message -> {
|
||||
try {
|
||||
if (properties.getClosest().getLat() != null && properties.getClosest().getLon() != null) {
|
||||
@@ -80,12 +80,12 @@ public class TwitterTrendFunctionConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Message<?>, Message<byte[]>> twitterTrendFunction(
|
||||
Function<Object, Message<byte[]>> managedJson, Function<Message<?>, Trends> trend,
|
||||
TwitterTrendFunctionProperties properties, Function<Message<?>,
|
||||
List<Location>> closestOrAvailableTrends) {
|
||||
public Function<Message<?>, Message<byte[]>> twitterTrendFunction(Function<Object, Message<byte[]>> managedJson,
|
||||
Function<Message<?>, Trends> trend, TwitterTrendFunctionProperties properties,
|
||||
Function<Message<?>, List<Location>> closestOrAvailableTrends) {
|
||||
|
||||
return (properties.getTrendQueryType() == TwitterTrendFunctionProperties.TrendQueryType.trend) ?
|
||||
trend.andThen(managedJson) : closestOrAvailableTrends.andThen(managedJson);
|
||||
return (properties.getTrendQueryType() == TwitterTrendFunctionProperties.TrendQueryType.trend)
|
||||
? trend.andThen(managedJson) : closestOrAvailableTrends.andThen(managedJson);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,10 +33,12 @@ public class TwitterTrendFunctionProperties {
|
||||
private static final Expression DEFAULT_EXPRESSION = new SpelExpressionParser().parseExpression("payload");
|
||||
|
||||
enum TrendQueryType {
|
||||
|
||||
/** Retrieve trending places. */
|
||||
trend,
|
||||
/** Retrieve the Locations of trending places. */
|
||||
trendLocation
|
||||
|
||||
}
|
||||
|
||||
private TrendQueryType trendQueryType = TrendQueryType.trend;
|
||||
@@ -74,17 +76,18 @@ public class TwitterTrendFunctionProperties {
|
||||
}
|
||||
|
||||
public static class Closest {
|
||||
|
||||
/**
|
||||
* If provided with a long parameter the available trend locations will be sorted by distance, nearest
|
||||
* to furthest, to the co-ordinate pair.
|
||||
* The valid ranges for longitude is -180.0 to +180.0 (West is negative, East is positive) inclusive.
|
||||
* If provided with a long parameter the available trend locations will be sorted
|
||||
* by distance, nearest to furthest, to the co-ordinate pair. The valid ranges for
|
||||
* longitude is -180.0 to +180.0 (West is negative, East is positive) inclusive.
|
||||
*/
|
||||
private Expression lat;
|
||||
|
||||
/**
|
||||
* If provided with a lat parameter the available trend locations will be sorted by distance, nearest to
|
||||
* furthest, to the co-ordinate pair. The valid ranges for longitude is -180.0 to +180.0 (West is negative,
|
||||
* East is positive) inclusive.
|
||||
* If provided with a lat parameter the available trend locations will be sorted
|
||||
* by distance, nearest to furthest, to the co-ordinate pair. The valid ranges for
|
||||
* longitude is -180.0 to +180.0 (West is negative, East is positive) inclusive.
|
||||
*/
|
||||
private Expression lon;
|
||||
|
||||
@@ -103,5 +106,7 @@ public class TwitterTrendFunctionProperties {
|
||||
public void setLon(Expression lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Configuration
|
||||
@@ -88,11 +87,12 @@ public class TwitterUsersFunctionConfiguration {
|
||||
|
||||
@Bean
|
||||
/**
|
||||
* queryUsers - depends on the `twitter.users.type` property is either userSearch or userLookup.
|
||||
* managedJson - converts Users into JSON message payload.
|
||||
* queryUsers - depends on the `twitter.users.type` property is either userSearch or
|
||||
* userLookup. managedJson - converts Users into JSON message payload.
|
||||
*/
|
||||
public Function<Message<?>, Message<byte[]>> twitterUsersFunction(Function<Message<?>, List<User>> queryUsers,
|
||||
Function<Object, Message<byte[]>> managedJson) {
|
||||
return queryUsers.andThen(managedJson);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ public class TwitterUsersFunctionProperties {
|
||||
private static final Expression DEFAULT_EXPRESSION = new SpelExpressionParser().parseExpression("payload");
|
||||
|
||||
public enum UserQueryType {
|
||||
|
||||
/** User retrieval types. */
|
||||
search, lookup
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,14 +47,14 @@ public class TwitterUsersFunctionProperties {
|
||||
private UserQueryType type = UserQueryType.search;
|
||||
|
||||
/**
|
||||
* Returns fully-hydrated user objects for specified by comma-separated values passed to the user_id and/or
|
||||
* screen_name parameters.
|
||||
* Returns fully-hydrated user objects for specified by comma-separated values passed
|
||||
* to the user_id and/or screen_name parameters.
|
||||
*/
|
||||
private final Lookup lookup = new Lookup();
|
||||
|
||||
/**
|
||||
* relevance-based search interface for querying by topical interest, full name, company name, location,
|
||||
* or other criteria.
|
||||
* relevance-based search interface for querying by topical interest, full name,
|
||||
* company name, location, or other criteria.
|
||||
*/
|
||||
private final Search search = new Search();
|
||||
|
||||
@@ -85,6 +87,7 @@ public class TwitterUsersFunctionProperties {
|
||||
}
|
||||
|
||||
public static class Lookup {
|
||||
|
||||
/**
|
||||
* A comma separated list of user IDs, up to 100 are allowed in a single request.
|
||||
* You are strongly encouraged to use a POST for larger requests.
|
||||
@@ -92,8 +95,9 @@ public class TwitterUsersFunctionProperties {
|
||||
private Expression userId;
|
||||
|
||||
/**
|
||||
* A comma separated list of screen names, up to 100 are allowed in a single request.
|
||||
* You are strongly encouraged to use a POST for larger (up to 100 screen names) requests.
|
||||
* A comma separated list of screen names, up to 100 are allowed in a single
|
||||
* request. You are strongly encouraged to use a POST for larger (up to 100 screen
|
||||
* names) requests.
|
||||
*/
|
||||
private Expression screenName;
|
||||
|
||||
@@ -112,9 +116,11 @@ public class TwitterUsersFunctionProperties {
|
||||
public void setScreenName(Expression screenName) {
|
||||
this.screenName = screenName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Search {
|
||||
|
||||
/**
|
||||
* The search query to run against people search.
|
||||
*/
|
||||
@@ -140,5 +146,7 @@ public class TwitterUsersFunctionProperties {
|
||||
public void setPage(int page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -58,14 +58,10 @@ import static org.mockserver.verify.VerificationTimes.once;
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"twitter.connection.consumerKey=consumerKey666",
|
||||
"twitter.connection.consumerSecret=consumerSecret666",
|
||||
"twitter.connection.accessToken=accessToken666",
|
||||
"twitter.connection.accessTokenSecret=accessTokenSecret666"
|
||||
})
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = { "twitter.connection.consumerKey=consumerKey666",
|
||||
"twitter.connection.consumerSecret=consumerSecret666", "twitter.connection.accessToken=accessToken666",
|
||||
"twitter.connection.accessTokenSecret=accessTokenSecret666" })
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
public abstract class TwitterGeoFunctionTest {
|
||||
|
||||
@@ -83,18 +79,12 @@ public abstract class TwitterGeoFunctionTest {
|
||||
public static void recordRequestExpectation(Map<String, List<String>> parameters) {
|
||||
|
||||
mockClient
|
||||
.when(
|
||||
request()
|
||||
.withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameters(parameters),
|
||||
unlimited())
|
||||
.respond(
|
||||
response()
|
||||
.withStatusCode(200)
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withBody(TwitterTestUtils.asString("classpath:/response/search_places_amsterdam.json"))
|
||||
.withDelay(TimeUnit.SECONDS, 1));
|
||||
.when(request().withMethod("GET").withPath("/geo/search.json").withQueryStringParameters(parameters),
|
||||
unlimited())
|
||||
.respond(response().withStatusCode(200)
|
||||
.withHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.withBody(TwitterTestUtils.asString("classpath:/response/search_places_amsterdam.json"))
|
||||
.withDelay(TimeUnit.SECONDS, 1));
|
||||
|
||||
}
|
||||
|
||||
@@ -109,10 +99,8 @@ public abstract class TwitterGeoFunctionTest {
|
||||
mockServer.stop();
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.geo.search.ip='127.0.0.1'",
|
||||
"twitter.geo.search.query=payload.toUpperCase()"
|
||||
})
|
||||
@TestPropertySource(
|
||||
properties = { "twitter.geo.search.ip='127.0.0.1'", "twitter.geo.search.query=payload.toUpperCase()" })
|
||||
public static class TwitterGeoSearchByIPAndQueryTests extends TwitterGeoFunctionTest {
|
||||
|
||||
@Test
|
||||
@@ -128,12 +116,10 @@ public abstract class TwitterGeoFunctionTest {
|
||||
|
||||
Message<?> received = twitterUsersFunction.apply(MessageBuilder.withPayload(inPayload).build());
|
||||
|
||||
mockClient.verify(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("ip", "127.0.0.1")
|
||||
.withQueryStringParameter("query", "AMSTERDAM"),
|
||||
once());
|
||||
mockClient.verify(request().withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("ip", "127.0.0.1")
|
||||
.withQueryStringParameter("query", "AMSTERDAM"), once());
|
||||
|
||||
String outPayload = new String((byte[]) received.getPayload());
|
||||
|
||||
@@ -142,13 +128,11 @@ public abstract class TwitterGeoFunctionTest {
|
||||
List places = new ObjectMapper().readValue(outPayload, List.class);
|
||||
assertThat(places).hasSize(12);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.geo.location.lat='52.378'",
|
||||
"twitter.geo.location.lon='4.9'",
|
||||
"twitter.geo.search.query=payload.toUpperCase()"
|
||||
})
|
||||
@TestPropertySource(properties = { "twitter.geo.location.lat='52.378'", "twitter.geo.location.lon='4.9'",
|
||||
"twitter.geo.search.query=payload.toUpperCase()" })
|
||||
public static class TwitterGeoSearchByLocationTests extends TwitterGeoFunctionTest {
|
||||
|
||||
@Test
|
||||
@@ -165,13 +149,11 @@ public abstract class TwitterGeoFunctionTest {
|
||||
|
||||
Message<?> received = twitterUsersFunction.apply(MessageBuilder.withPayload(inPayload).build());
|
||||
|
||||
mockClient.verify(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("lat", "52.378")
|
||||
.withQueryStringParameter("long", "4.9")
|
||||
.withQueryStringParameter("query", "Amsterdam"),
|
||||
once());
|
||||
mockClient.verify(request().withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("lat", "52.378")
|
||||
.withQueryStringParameter("long", "4.9")
|
||||
.withQueryStringParameter("query", "Amsterdam"), once());
|
||||
|
||||
String outPayload = new String((byte[]) received.getPayload());
|
||||
|
||||
@@ -180,13 +162,11 @@ public abstract class TwitterGeoFunctionTest {
|
||||
List places = new ObjectMapper().readValue(outPayload, List.class);
|
||||
assertThat(places).hasSize(12);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.geo.type=reverse",
|
||||
"twitter.geo.location.lat='52.378'",
|
||||
"twitter.geo.location.lon='4.9'"
|
||||
})
|
||||
@TestPropertySource(properties = { "twitter.geo.type=reverse", "twitter.geo.location.lat='52.378'",
|
||||
"twitter.geo.location.lon='4.9'" })
|
||||
public static class TwitterGeoSearchByLocation2Tests extends TwitterGeoFunctionTest {
|
||||
|
||||
@Test
|
||||
@@ -202,12 +182,10 @@ public abstract class TwitterGeoFunctionTest {
|
||||
|
||||
Message<?> received = twitterUsersFunction.apply(MessageBuilder.withPayload(inPayload).build());
|
||||
|
||||
mockClient.verify(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("lat", "52.378")
|
||||
.withQueryStringParameter("long", "4.9"),
|
||||
once());
|
||||
mockClient.verify(request().withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("lat", "52.378")
|
||||
.withQueryStringParameter("long", "4.9"), once());
|
||||
|
||||
String outPayload = new String((byte[]) received.getPayload());
|
||||
|
||||
@@ -216,13 +194,12 @@ public abstract class TwitterGeoFunctionTest {
|
||||
List places = new ObjectMapper().readValue(outPayload, List.class);
|
||||
assertThat(places).hasSize(12);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.geo.location.lat=#jsonPath(new String(payload),'$.location.lat')",
|
||||
@TestPropertySource(properties = { "twitter.geo.location.lat=#jsonPath(new String(payload),'$.location.lat')",
|
||||
"twitter.geo.location.lon=#jsonPath(new String(payload),'$.location.lon')",
|
||||
"twitter.geo.search.query=#jsonPath(new String(payload),'$.country')"
|
||||
})
|
||||
"twitter.geo.search.query=#jsonPath(new String(payload),'$.country')" })
|
||||
public static class TwitterGeoSearchJsonPathTests extends TwitterGeoFunctionTest {
|
||||
|
||||
@Test
|
||||
@@ -237,18 +214,15 @@ public abstract class TwitterGeoFunctionTest {
|
||||
|
||||
String inPayload = "{ \"country\" : \"Netherlands\", \"location\" : { \"lat\" : 52.00 , \"lon\" : 5.0 } }";
|
||||
|
||||
Message<?> received = twitterUsersFunction.apply(MessageBuilder
|
||||
.withPayload(inPayload)
|
||||
.setHeader("contentType", MimeTypeUtils.APPLICATION_JSON_VALUE)
|
||||
.build());
|
||||
Message<?> received = twitterUsersFunction.apply(MessageBuilder.withPayload(inPayload)
|
||||
.setHeader("contentType", MimeTypeUtils.APPLICATION_JSON_VALUE)
|
||||
.build());
|
||||
|
||||
mockClient.verify(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("lat", "52.0")
|
||||
.withQueryStringParameter("long", "5.0")
|
||||
.withQueryStringParameter("query", "Netherlands"),
|
||||
once());
|
||||
mockClient.verify(request().withMethod("GET")
|
||||
.withPath("/geo/search.json")
|
||||
.withQueryStringParameter("lat", "52.0")
|
||||
.withQueryStringParameter("long", "5.0")
|
||||
.withQueryStringParameter("query", "Netherlands"), once());
|
||||
|
||||
String outPayload = new String((byte[]) received.getPayload());
|
||||
|
||||
@@ -257,23 +231,26 @@ public abstract class TwitterGeoFunctionTest {
|
||||
List places = new ObjectMapper().readValue(outPayload, List.class);
|
||||
assertThat(places).hasSize(12);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Import(TwitterGeoFunctionConfiguration.class)
|
||||
public static class TwitterGeoFunctionTestApplication {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public twitter4j.conf.Configuration twitterConfiguration2(TwitterConnectionProperties properties,
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> toConfigurationBuilder) {
|
||||
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> mockedConfiguration =
|
||||
toConfigurationBuilder.andThen(
|
||||
new TwitterTestUtils().mockTwitterUrls(
|
||||
String.format("http://%s:%s", MOCK_SERVER_IP, MOCK_SERVER_PORT)));
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> mockedConfiguration = toConfigurationBuilder
|
||||
.andThen(new TwitterTestUtils()
|
||||
.mockTwitterUrls(String.format("http://%s:%s", MOCK_SERVER_IP, MOCK_SERVER_PORT)));
|
||||
|
||||
return mockedConfiguration.apply(properties).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,14 +52,10 @@ import static org.mockserver.verify.VerificationTimes.once;
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"twitter.connection.consumerKey=consumerKey666",
|
||||
"twitter.connection.consumerSecret=consumerSecret666",
|
||||
"twitter.connection.accessToken=accessToken666",
|
||||
"twitter.connection.accessTokenSecret=accessTokenSecret666"
|
||||
})
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = { "twitter.connection.consumerKey=consumerKey666",
|
||||
"twitter.connection.consumerSecret=consumerSecret666", "twitter.connection.accessToken=accessToken666",
|
||||
"twitter.connection.accessTokenSecret=accessTokenSecret666" })
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
public abstract class TwitterTrendFunctionTests {
|
||||
|
||||
@@ -70,6 +66,7 @@ public abstract class TwitterTrendFunctionTests {
|
||||
private static ClientAndServer mockServer;
|
||||
|
||||
private static MockServerClient mockClient;
|
||||
|
||||
private static HttpRequest trendsRequest;
|
||||
|
||||
@Autowired
|
||||
@@ -80,10 +77,8 @@ public abstract class TwitterTrendFunctionTests {
|
||||
mockServer = ClientAndServer.startClientAndServer(MOCK_SERVER_PORT);
|
||||
mockClient = new MockServerClient(MOCK_SERVER_IP, MOCK_SERVER_PORT);
|
||||
|
||||
trendsRequest = setExpectation(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/trends/place.json")
|
||||
.withQueryStringParameter("id", "2972"));
|
||||
trendsRequest = setExpectation(
|
||||
request().withMethod("GET").withPath("/trends/place.json").withQueryStringParameter("id", "2972"));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@@ -92,23 +87,16 @@ public abstract class TwitterTrendFunctionTests {
|
||||
}
|
||||
|
||||
public static HttpRequest setExpectation(HttpRequest request) {
|
||||
mockClient
|
||||
.when(request, exactly(1))
|
||||
.respond(response()
|
||||
.withStatusCode(200)
|
||||
.withHeaders(
|
||||
new Header("Content-Type", "application/json; charset=utf-8"),
|
||||
new Header("Cache-Control", "public, max-age=86400"))
|
||||
.withBody(TwitterTestUtils.asString("classpath:/response/trends.json"))
|
||||
.withDelay(TimeUnit.SECONDS, 1)
|
||||
);
|
||||
mockClient.when(request, exactly(1))
|
||||
.respond(response().withStatusCode(200)
|
||||
.withHeaders(new Header("Content-Type", "application/json; charset=utf-8"),
|
||||
new Header("Cache-Control", "public, max-age=86400"))
|
||||
.withBody(TwitterTestUtils.asString("classpath:/response/trends.json"))
|
||||
.withDelay(TimeUnit.SECONDS, 1));
|
||||
return request;
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.trend.locationId='2972'",
|
||||
"twitter.connection.rawJson=true"
|
||||
})
|
||||
@TestPropertySource(properties = { "twitter.trend.locationId='2972'", "twitter.connection.rawJson=true" })
|
||||
public static class TwitterTrendPayloadTests extends TwitterTrendFunctionTests {
|
||||
|
||||
@Test
|
||||
@@ -117,24 +105,26 @@ public abstract class TwitterTrendFunctionTests {
|
||||
mockClient.verify(trendsRequest, once());
|
||||
assertThat(received).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Import(TwitterTrendFunctionConfiguration.class)
|
||||
public static class TwitterTrendFunctionTestApplication {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public twitter4j.conf.Configuration twitterConfiguration2(TwitterConnectionProperties properties,
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> toConfigurationBuilder) {
|
||||
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> mockedConfiguration =
|
||||
toConfigurationBuilder.andThen(
|
||||
new TwitterTestUtils().mockTwitterUrls(
|
||||
String.format("http://%s:%s", MOCK_SERVER_IP, MOCK_SERVER_PORT)));
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> mockedConfiguration = toConfigurationBuilder
|
||||
.andThen(new TwitterTestUtils()
|
||||
.mockTwitterUrls(String.format("http://%s:%s", MOCK_SERVER_IP, MOCK_SERVER_PORT)));
|
||||
|
||||
return mockedConfiguration.apply(properties).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,14 +55,10 @@ import static org.mockserver.verify.VerificationTimes.once;
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"twitter.connection.consumerKey=consumerKey666",
|
||||
"twitter.connection.consumerSecret=consumerSecret666",
|
||||
"twitter.connection.accessToken=accessToken666",
|
||||
"twitter.connection.accessTokenSecret=accessTokenSecret666"
|
||||
})
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = { "twitter.connection.consumerKey=consumerKey666",
|
||||
"twitter.connection.consumerSecret=consumerSecret666", "twitter.connection.accessToken=accessToken666",
|
||||
"twitter.connection.accessTokenSecret=accessTokenSecret666" })
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
public abstract class TwitterUsersFunctionTests {
|
||||
|
||||
@@ -73,8 +69,11 @@ public abstract class TwitterUsersFunctionTests {
|
||||
private static ClientAndServer mockServer;
|
||||
|
||||
private static MockServerClient mockClient;
|
||||
|
||||
private static HttpRequest searchUsersRequest;
|
||||
|
||||
private static HttpRequest lookupUsersRequest;
|
||||
|
||||
private static HttpRequest lookupUsersRequest2;
|
||||
|
||||
@Autowired
|
||||
@@ -84,16 +83,12 @@ public abstract class TwitterUsersFunctionTests {
|
||||
Function<Message<?>, Message<byte[]>> twitterUsersFunction;
|
||||
|
||||
public static HttpRequest setExpectation(HttpRequest request, String responseUri) {
|
||||
mockClient
|
||||
.when(request, exactly(1))
|
||||
.respond(response()
|
||||
.withStatusCode(200)
|
||||
.withHeaders(
|
||||
new Header("Content-Type", "application/json; charset=utf-8"),
|
||||
new Header("Cache-Control", "public, max-age=86400"))
|
||||
.withBody(TwitterTestUtils.asString(responseUri))
|
||||
.withDelay(TimeUnit.SECONDS, 1)
|
||||
);
|
||||
mockClient.when(request, exactly(1))
|
||||
.respond(response().withStatusCode(200)
|
||||
.withHeaders(new Header("Content-Type", "application/json; charset=utf-8"),
|
||||
new Header("Cache-Control", "public, max-age=86400"))
|
||||
.withBody(TwitterTestUtils.asString(responseUri))
|
||||
.withDelay(TimeUnit.SECONDS, 1));
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -102,25 +97,20 @@ public abstract class TwitterUsersFunctionTests {
|
||||
mockServer = ClientAndServer.startClientAndServer(MOCK_SERVER_PORT);
|
||||
mockClient = new MockServerClient(MOCK_SERVER_IP, MOCK_SERVER_PORT);
|
||||
|
||||
searchUsersRequest = setExpectation(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/users/search.json")
|
||||
.withQueryStringParameter("q", "tzolov")
|
||||
.withQueryStringParameter("page", "3"),
|
||||
"classpath:/response/search_users.json");
|
||||
searchUsersRequest = setExpectation(request().withMethod("GET")
|
||||
.withPath("/users/search.json")
|
||||
.withQueryStringParameter("q", "tzolov")
|
||||
.withQueryStringParameter("page", "3"), "classpath:/response/search_users.json");
|
||||
|
||||
lookupUsersRequest = setExpectation(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/users/lookup.json")
|
||||
.withQueryStringParameter("user_id",
|
||||
"710705860343963648,326896547,267603736,781497571629989888,838754923"),
|
||||
lookupUsersRequest = setExpectation(request().withMethod("GET")
|
||||
.withPath("/users/lookup.json")
|
||||
.withQueryStringParameter("user_id", "710705860343963648,326896547,267603736,781497571629989888,838754923"),
|
||||
"classpath:/response/lookup_users_id.json");
|
||||
|
||||
lookupUsersRequest2 = setExpectation(request()
|
||||
.withMethod("GET")
|
||||
.withPath("/users/lookup.json")
|
||||
.withQueryStringParameter("screen_name",
|
||||
"TzolovMarto,Rabotnik57,antzolov,peyo_tzolov,ivantzolov"),
|
||||
lookupUsersRequest2 = setExpectation(
|
||||
request().withMethod("GET")
|
||||
.withPath("/users/lookup.json")
|
||||
.withQueryStringParameter("screen_name", "TzolovMarto,Rabotnik57,antzolov,peyo_tzolov,ivantzolov"),
|
||||
"classpath:/response/lookup_users_id.json");
|
||||
}
|
||||
|
||||
@@ -129,10 +119,7 @@ public abstract class TwitterUsersFunctionTests {
|
||||
mockServer.stop();
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.users.type=search",
|
||||
"twitter.users.search.query=payload"
|
||||
})
|
||||
@TestPropertySource(properties = { "twitter.users.type=search", "twitter.users.search.query=payload" })
|
||||
public static class TwitterSearchUsersTests extends TwitterUsersFunctionTests {
|
||||
|
||||
@Test
|
||||
@@ -143,12 +130,11 @@ public abstract class TwitterUsersFunctionTests {
|
||||
List list = mapper.readValue(new String((byte[]) received.getPayload()), List.class);
|
||||
assertThat(list).hasSize(20);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.users.type=lookup",
|
||||
"twitter.users.lookup.userId='710705860343963648,326896547,267603736,781497571629989888,838754923'"
|
||||
})
|
||||
@TestPropertySource(properties = { "twitter.users.type=lookup",
|
||||
"twitter.users.lookup.userId='710705860343963648,326896547,267603736,781497571629989888,838754923'" })
|
||||
public static class TwitterLookupUserIdLiteralTests extends TwitterUsersFunctionTests {
|
||||
|
||||
@Test
|
||||
@@ -161,12 +147,11 @@ public abstract class TwitterUsersFunctionTests {
|
||||
List list = mapper.readValue(new String((byte[]) received.getPayload()), List.class);
|
||||
assertThat(list).hasSize(5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"twitter.users.type=lookup",
|
||||
"twitter.users.lookup.screenName=#jsonPath(payload,'$[*].code')"
|
||||
})
|
||||
@TestPropertySource(properties = { "twitter.users.type=lookup",
|
||||
"twitter.users.lookup.screenName=#jsonPath(payload,'$[*].code')" })
|
||||
public static class TwitterLookupScreenNamePayloadTests extends TwitterUsersFunctionTests {
|
||||
|
||||
@Test
|
||||
@@ -182,23 +167,26 @@ public abstract class TwitterUsersFunctionTests {
|
||||
List list = mapper.readValue(new String((byte[]) received.getPayload()), List.class);
|
||||
assertThat(list).hasSize(5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Import(TwitterUsersFunctionConfiguration.class)
|
||||
static class TwitterUsersFunctionTestApplication {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public twitter4j.conf.Configuration twitterConfiguration2(TwitterConnectionProperties properties,
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> toConfigurationBuilder) {
|
||||
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> mockedConfiguration =
|
||||
toConfigurationBuilder.andThen(
|
||||
new TwitterTestUtils().mockTwitterUrls(
|
||||
String.format("http://%s:%s", MOCK_SERVER_IP, MOCK_SERVER_PORT)));
|
||||
Function<TwitterConnectionProperties, ConfigurationBuilder> mockedConfiguration = toConfigurationBuilder
|
||||
.andThen(new TwitterTestUtils()
|
||||
.mockTwitterUrls(String.format("http://%s:%s", MOCK_SERVER_IP, MOCK_SERVER_PORT)));
|
||||
|
||||
return mockedConfiguration.apply(properties).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user