diff --git a/src/main/java/com/proofpoint/event/collector/EventTapWriter.java b/src/main/java/com/proofpoint/event/collector/EventTapWriter.java index a83d699..d610cec 100644 --- a/src/main/java/com/proofpoint/event/collector/EventTapWriter.java +++ b/src/main/java/com/proofpoint/event/collector/EventTapWriter.java @@ -16,12 +16,20 @@ package com.proofpoint.event.collector; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Iterables; +import com.google.common.collect.Table; +import com.google.common.collect.Table.Cell; import com.proofpoint.discovery.client.ServiceDescriptor; import com.proofpoint.discovery.client.ServiceSelector; import com.proofpoint.discovery.client.ServiceType; import com.proofpoint.event.collector.EventTapWriter.EventTypePolicy.FlowPolicy; +import com.proofpoint.event.collector.EventTapWriter.FlowInfo.Builder; +import com.proofpoint.event.collector.StaticEventTapConfig.FlowKey; import com.proofpoint.log.Logger; import com.proofpoint.units.Duration; import org.weakref.jmx.Managed; @@ -31,7 +39,6 @@ import javax.inject.Inject; import java.io.IOException; import java.net.URI; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -44,12 +51,24 @@ import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; +import static com.proofpoint.event.collector.QosDelivery.BEST_EFFORT; +import static com.proofpoint.event.collector.QosDelivery.RETRY; +import static java.lang.String.format; public class EventTapWriter implements EventWriter { @VisibleForTesting static final String FLOW_ID_PROPERTY_NAME = "flowId"; + @VisibleForTesting + static final String HTTP_PROPERTY_NAME = "http"; + + @VisibleForTesting + static final String EVENT_TYPE_PROPERTY_NAME = "eventType"; + + private static final String HTTPS_PROPERTY_NAME = "https"; + private static final String QOS_DELIVERY_PROPERTY_NAME = "qos.delivery"; + private static final Logger log = Logger.get(EventTapWriter.class); private static final EventTypePolicy NULL_EVENT_TYPE_POLICY = new EventTypePolicy.Builder().build(); private final ServiceSelector selector; @@ -60,7 +79,8 @@ public class EventTapWriter implements EventWriter private final AtomicReference> eventTypePolicies = new AtomicReference>( ImmutableMap.of()); - private Map> flows = ImmutableMap.of(); + private final List staticTapSpecs; + private Table flows = ImmutableTable.of(); private ScheduledFuture refreshJob; private final Duration flowRefreshDuration; @@ -70,8 +90,10 @@ public EventTapWriter(@ServiceType("eventTap") ServiceSelector selector, @EventTap ScheduledExecutorService executorService, BatchProcessorFactory batchProcessorFactory, EventTapFlowFactory eventTapFlowFactory, - EventTapConfig config) + EventTapConfig config, + StaticEventTapConfig staticEventTapConfig) { + this.staticTapSpecs = createTapSpecFromConfig(checkNotNull(staticEventTapConfig, "staticEventTapConfig is null")); this.selector = checkNotNull(selector, "selector is null"); this.executorService = checkNotNull(executorService, "executorService is null"); this.flowRefreshDuration = checkNotNull(config, "config is null").getEventTapRefreshDuration(); @@ -125,20 +147,18 @@ public void refreshFlows() { try { Map existingPolicies = eventTypePolicies.get(); - Map> existingFlows = flows; + Table existingFlows = flows; ImmutableMap.Builder policiesBuilder = ImmutableMap.builder(); - Map> newFlows = constructFlowInfoFromDiscovery(); + Table newFlows = constructFlowInfoFromTapSpec(Iterables.concat(staticTapSpecs, createTapSpecFromDiscovery(selector.selectAllServices()))); if (existingFlows.equals(newFlows)) { return; } - for (Map.Entry> flowsForEventTypeEntry : newFlows.entrySet()) { - String eventType = flowsForEventTypeEntry.getKey(); - Map flowsForEventType = flowsForEventTypeEntry.getValue(); + for (Map.Entry> entry : newFlows.rowMap().entrySet()) { + String eventType = entry.getKey(); EventTypePolicy existingPolicy = firstNonNull(existingPolicies.get(eventType), NULL_EVENT_TYPE_POLICY); - - policiesBuilder.put(eventType, constructPolicyForFlows(existingPolicy, eventType, flowsForEventType)); + policiesBuilder.put(eventType, constructPolicyForFlows(existingPolicy, eventType, entry.getValue())); } Map newPolicies = policiesBuilder.build(); @@ -150,7 +170,6 @@ public void refreshFlows() catch (Exception e) { log.error(e, "Couldn't refresh flows"); } - } @Override @@ -168,65 +187,39 @@ public void distribute(Event event) write(event); } - private Map> constructFlowInfoFromDiscovery() + private Table constructFlowInfoFromTapSpec(Iterable tapSpecs) { - List descriptors = selector.selectAllServices(); - // First level is EventType, second is flowId - Map> flows = new HashMap<>(); - - for (ServiceDescriptor descriptor : descriptors) { - Map properties = descriptor.getProperties(); - String eventType = properties.get("eventType"); - String flowId = properties.get(FLOW_ID_PROPERTY_NAME); + Table flows = HashBasedTable.create(); - URI uri = safeUriFromString(properties.get("https")); - if (uri == null && allowHttpConsumers) { - uri = safeUriFromString(properties.get("http")); - } + for (TapSpec tapSpec : tapSpecs) { + String eventType = tapSpec.getEventType(); + String flowId = tapSpec.getFlowId(); - String qosDelivery = properties.get("qos.delivery"); + URI uri = tapSpec.getUri(); - if (isNullOrEmpty(eventType) || isNullOrEmpty(flowId) || uri == null) { - continue; - } - - Map flowsForEventType = flows.get(eventType); - if (flowsForEventType == null) { - flowsForEventType = new HashMap<>(); - flows.put(eventType, flowsForEventType); - } - - FlowInfo.Builder flowBuilder = flowsForEventType.get(flowId); + FlowInfo.Builder flowBuilder = flows.get(eventType, flowId); if (flowBuilder == null) { flowBuilder = FlowInfo.builder(); - flowsForEventType.put(flowId, flowBuilder); + flows.put(eventType, flowId, flowBuilder); } - if ("retry".equalsIgnoreCase(qosDelivery)) { + QosDelivery qosDelivery = tapSpec.getQosDelivery(); + if (RETRY.equals(qosDelivery)) { flowBuilder.setQosEnabled(true); } flowBuilder.addDestination(uri); } - return constructFlowsFromBuilderMap(flows); + return constructFlowsFromTable(flows); } - private Map> constructFlowsFromBuilderMap(Map> flows) + private Table constructFlowsFromTable(Table flows) { - ImmutableMap.Builder> flowsBuilder = ImmutableMap.builder(); - for (Entry> flowsEntry : flows.entrySet()) { - String eventType = flowsEntry.getKey(); - Map flowsForEventType = flowsEntry.getValue(); - - ImmutableMap.Builder flowsBuilderForEventType = ImmutableMap.builder(); - for (Entry flowsForEventTypeEntry : flowsForEventType.entrySet()) { - String flowId = flowsForEventTypeEntry.getKey(); - FlowInfo.Builder flowBuilder = flowsForEventTypeEntry.getValue(); - flowsBuilderForEventType.put(flowId, flowBuilder.build()); - } + ImmutableTable.Builder flowsBuilder = ImmutableTable.builder(); - flowsBuilder.put(eventType, flowsBuilderForEventType.build()); + for (Cell cell : flows.cellSet()) { + flowsBuilder.put(cell.getRowKey(), cell.getColumnKey(), cell.getValue().build()); } return flowsBuilder.build(); @@ -310,6 +303,47 @@ private void stopBatchProcessor(String eventType, String flowId, BatchProcessor< processor.stop(); } + private List createTapSpecFromDiscovery(Iterable descriptors) + { + ImmutableList.Builder tapSpecBuilder = ImmutableList.builder(); + for (ServiceDescriptor descriptor : descriptors) { + + Map properties = descriptor.getProperties(); + String eventType = properties.get(EVENT_TYPE_PROPERTY_NAME); + String flowId = properties.get(FLOW_ID_PROPERTY_NAME); + + URI uri = safeUriFromString(properties.get(HTTPS_PROPERTY_NAME)); + if (uri == null && allowHttpConsumers) { + uri = safeUriFromString(properties.get(HTTP_PROPERTY_NAME)); + } + + if (isNullOrEmpty(eventType) || isNullOrEmpty(flowId) || uri == null) { + continue; + } + + String qosDeliveryString = firstNonNull(properties.get(QOS_DELIVERY_PROPERTY_NAME), BEST_EFFORT.toString()); + TapSpec tapSpec = new TapSpec(eventType, flowId, uri, QosDelivery.fromString(qosDeliveryString)); + + tapSpecBuilder.add(tapSpec); + } + + return tapSpecBuilder.build(); + } + + private static List createTapSpecFromConfig(StaticEventTapConfig staticEventTapConfig) + { + ImmutableList.Builder tapSpecBuilder = ImmutableList.builder(); + for (Entry entry : staticEventTapConfig.getStaticTaps().entrySet()) { + FlowKey flowKey = entry.getKey(); + PerFlowStaticEventTapConfig config = entry.getValue(); + for (String uri : config.getUris()) { + tapSpecBuilder.add(new TapSpec(flowKey.getEventType(), flowKey.getFlowId(), safeUriFromString(uri), config.getQosDelivery())); + } + } + + return tapSpecBuilder.build(); + } + private static URI safeUriFromString(String uri) { try { @@ -322,7 +356,7 @@ private static URI safeUriFromString(String uri) private static String createBatchProcessorName(String eventType, String flowId) { - return String.format("%s{%s}", eventType, flowId); + return format("%s{%s}", eventType, flowId); } static class FlowInfo @@ -415,4 +449,40 @@ public EventTypePolicy build() } } } + + private static class TapSpec + { + private String eventType; + private String flowId; + private URI uri; + private QosDelivery qosDelivery; + + public TapSpec(String eventType, String flowId, URI uri, QosDelivery qosDelivery) + { + this.eventType = checkNotNull(eventType, "eventType is null"); + this.flowId = checkNotNull(flowId, "flowId is null"); + this.uri = checkNotNull(uri, "uri is null"); + this.qosDelivery = checkNotNull(qosDelivery, "qosDelivery is null"); + } + + public String getEventType() + { + return eventType; + } + + public String getFlowId() + { + return flowId; + } + + public URI getUri() + { + return uri; + } + + public QosDelivery getQosDelivery() + { + return qosDelivery; + } + } } diff --git a/src/main/java/com/proofpoint/event/collector/MainModule.java b/src/main/java/com/proofpoint/event/collector/MainModule.java index 1be19fc..af07313 100644 --- a/src/main/java/com/proofpoint/event/collector/MainModule.java +++ b/src/main/java/com/proofpoint/event/collector/MainModule.java @@ -82,6 +82,7 @@ public void configure(Binder binder) binder.bind(CombineObjectMetadataStore.class).to(S3CombineObjectMetadataStore.class).in(Scopes.SINGLETON); bindConfig(binder).to(ServerConfig.class); + bindConfig(binder).to(StaticEventTapConfig.class); eventBinder(binder).bindEventClient(CombineCompleted.class); @@ -185,7 +186,8 @@ private ScheduledExecutorService createScheduledCombinerLowPriorityExecutor(Serv @Provides @Singleton - private Ticker providesTicker() { + private Ticker providesTicker() + { return Ticker.systemTicker(); } } diff --git a/src/main/java/com/proofpoint/event/collector/PerFlowStaticEventTapConfig.java b/src/main/java/com/proofpoint/event/collector/PerFlowStaticEventTapConfig.java new file mode 100644 index 0000000..db280c2 --- /dev/null +++ b/src/main/java/com/proofpoint/event/collector/PerFlowStaticEventTapConfig.java @@ -0,0 +1,108 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.proofpoint.configuration.Config; +import com.proofpoint.configuration.ConfigDescription; +import com.proofpoint.event.collector.validation.ValidUri; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Set; + +import static com.google.common.base.Preconditions.*; +import static com.google.common.base.Strings.nullToEmpty; +import static com.proofpoint.event.collector.QosDelivery.BEST_EFFORT; + +public class PerFlowStaticEventTapConfig +{ + private static final Splitter COMMA_LIST_SPLITTER = Splitter.on(',') + .trimResults() + .omitEmptyStrings(); + + private QosDelivery qosDelivery = BEST_EFFORT; + private Set uris = ImmutableSet.of(); + + @ValidUri(schemes = {"http", "https"}) + @Size(min = 1, message = "may not be empty") + public Set getUris() + { + return uris; + } + + @Config("uris") + @ConfigDescription("Comma separated list of tap uris") + public PerFlowStaticEventTapConfig setUris(String urisString) + { + Iterable uriTokens = COMMA_LIST_SPLITTER.split(nullToEmpty(urisString)); + this.uris = ImmutableSet.copyOf(uriTokens); + return this; + } + + @VisibleForTesting + PerFlowStaticEventTapConfig setUris(Set uris) + { + this.uris = ImmutableSet.copyOf(checkNotNull(uris, "uris is null")); + return this; + } + + @NotNull + public QosDelivery getQosDelivery() + { + return qosDelivery; + } + + @Config("qos-delivery") + @ConfigDescription("The Quality of Service the collector will provide when delivering events to this flow.") + public PerFlowStaticEventTapConfig setQosDelivery(QosDelivery qosDelivery) + { + this.qosDelivery = qosDelivery; + return this; + } + + @Override + public int hashCode() + { + return Objects.hashCode(qosDelivery, uris); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final PerFlowStaticEventTapConfig other = (PerFlowStaticEventTapConfig) obj; + return Objects.equal(this.qosDelivery, other.qosDelivery) && Objects.equal(this.uris, other.uris); + } + + @Override + public String toString() + { + return Objects.toStringHelper(this) + .add("qosDelivery", qosDelivery) + .add("uris", uris) + .toString(); + } + +} diff --git a/src/main/java/com/proofpoint/event/collector/QosDelivery.java b/src/main/java/com/proofpoint/event/collector/QosDelivery.java new file mode 100644 index 0000000..46837fe --- /dev/null +++ b/src/main/java/com/proofpoint/event/collector/QosDelivery.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector; + +import static com.google.common.base.CaseFormat.LOWER_CAMEL; +import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; + +public enum QosDelivery +{ + RETRY, + BEST_EFFORT; // default behavior + + public static QosDelivery fromString(String rawValue) + { + return Enum.valueOf(QosDelivery.class, LOWER_CAMEL.to(UPPER_UNDERSCORE, rawValue)); + } + + @Override + public String toString() + { + return UPPER_UNDERSCORE.to(LOWER_CAMEL, name()); + } +} diff --git a/src/main/java/com/proofpoint/event/collector/StaticEventTapConfig.java b/src/main/java/com/proofpoint/event/collector/StaticEventTapConfig.java new file mode 100644 index 0000000..250eee5 --- /dev/null +++ b/src/main/java/com/proofpoint/event/collector/StaticEventTapConfig.java @@ -0,0 +1,112 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector; + +import com.google.common.base.Objects; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.proofpoint.configuration.Config; +import com.proofpoint.configuration.ConfigDescription; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; + +public class StaticEventTapConfig +{ + private static final Splitter FLOW_KEY_SPLITTER = Splitter.on('@') + .trimResults(); + + private Map staticTaps = ImmutableMap.of(); + + @Valid + @NotNull + public Map getStaticTaps() + { + return staticTaps; + } + + @Config("static-taps") + @ConfigDescription("Event tap specification for static taps that are added to the dynamic taps found through discovery.") + public StaticEventTapConfig setStaticTaps(Map staticTaps) + { + this.staticTaps = staticTaps; + return this; + } + + public static final class FlowKey + { + private final String eventType; + private final String flowId; + + public FlowKey(String eventType, String flowId) + { + this.eventType = checkNotNull(eventType, "eventType is null"); + this.flowId = checkNotNull(flowId, "flowId is null"); + } + + public static FlowKey valueOf(String rawValue) + { + List tokens = FLOW_KEY_SPLITTER.splitToList(nullToEmpty(rawValue)); + checkArgument(tokens.size() == 2, "Invalid flow key: %s; Flow key must have a single @ character", rawValue); + checkArgument(!tokens.get(0).isEmpty() && !tokens.get(1).isEmpty(), "Invalid flow key: %s; Elements separate by @ cannot be empty", rawValue); + return new FlowKey(tokens.get(0), tokens.get(1)); + } + + public String getEventType() + { + return eventType; + } + + public String getFlowId() + { + return flowId; + } + + @Override + public int hashCode() + { + return Objects.hashCode(eventType, flowId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final FlowKey other = (FlowKey) obj; + return Objects.equal(this.eventType, other.eventType) && Objects.equal(this.flowId, other.flowId); + } + + @Override + public String toString() + { + return Objects.toStringHelper(this) + .add("eventType", eventType) + .add("flowId", flowId) + .toString(); + } + } +} diff --git a/src/main/java/com/proofpoint/event/collector/validation/ValidUri.java b/src/main/java/com/proofpoint/event/collector/validation/ValidUri.java new file mode 100644 index 0000000..ea15c19 --- /dev/null +++ b/src/main/java/com/proofpoint/event/collector/validation/ValidUri.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = ValidUriValidator.class) +public @interface ValidUri +{ + String[] schemes() default {}; + + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + diff --git a/src/main/java/com/proofpoint/event/collector/validation/ValidUriValidator.java b/src/main/java/com/proofpoint/event/collector/validation/ValidUriValidator.java new file mode 100644 index 0000000..a444431 --- /dev/null +++ b/src/main/java/com/proofpoint/event/collector/validation/ValidUriValidator.java @@ -0,0 +1,112 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector.validation; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Iterables; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.net.URI; +import java.util.Arrays; +import java.util.Set; + +public class ValidUriValidator + implements ConstraintValidator> +{ + private static final Joiner ERROR_JOINER = Joiner.on(", "); + private static final String ERROR_SEPARATOR = "; "; + + private static final Function TO_LOWER_CASE = new Function() + { + @Override + public String apply(String input) + { + return input.toLowerCase(); + } + }; + + private Set validSchemes; + + @Override + public void initialize(ValidUri constraintAnnotation) + { + validSchemes = ImmutableSet.copyOf(Iterables.transform(Arrays.asList(constraintAnnotation.schemes()), TO_LOWER_CASE)); + } + + @Override + public boolean isValid(Iterable value, ConstraintValidatorContext context) + { + ImmutableSortedSet.Builder uriWithInvalidSyntaxBuilder = ImmutableSortedSet.naturalOrder(); + ImmutableSortedSet.Builder uriWithInvalidSchemeBuilder = ImmutableSortedSet.naturalOrder(); + + for (String uriString : value) { + URI uri; + try { + uri = URI.create(uriString); + } + catch (Exception ignored) { + uriWithInvalidSyntaxBuilder.add(uriString); + continue; + } + + if (validSchemes.isEmpty()) { + continue; + } + + String uriScheme = uri.getScheme(); + + if (uriScheme == null || !validSchemes.contains(uriScheme.toLowerCase())) { + uriWithInvalidSchemeBuilder.add(uriString); + continue; + } + } + + StringBuilder messageBuilder = new StringBuilder(); + Set uriWithInvalidSyntax = uriWithInvalidSyntaxBuilder.build(); + Set uriWithInvalidScheme = uriWithInvalidSchemeBuilder.build(); + + if (uriWithInvalidSyntax.isEmpty() && uriWithInvalidScheme.isEmpty()) { + return true; + } + + messageBuilder.append("Invalid URIs: "); + String prefix = ""; + if (!uriWithInvalidSyntax.isEmpty()) { + messageBuilder + .append(prefix) + .append("Invalid syntax: ") + .append(ERROR_JOINER.join(uriWithInvalidSyntax)); + prefix = ERROR_SEPARATOR; + } + if (!uriWithInvalidScheme.isEmpty()) { + messageBuilder + .append(prefix) + .append("Invalid scheme: ") + .append(ERROR_JOINER.join(uriWithInvalidScheme)); + prefix = ERROR_SEPARATOR; + } + + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(messageBuilder.toString()) + .addConstraintViolation(); + + return false; + } +} diff --git a/src/test/java/com/proofpoint/event/collector/TestEventTapWriter.java b/src/test/java/com/proofpoint/event/collector/TestEventTapWriter.java index a4e6752..4ddf934 100644 --- a/src/test/java/com/proofpoint/event/collector/TestEventTapWriter.java +++ b/src/test/java/com/proofpoint/event/collector/TestEventTapWriter.java @@ -25,6 +25,7 @@ import com.proofpoint.discovery.client.ServiceState; import com.proofpoint.discovery.client.testing.StaticServiceSelector; import com.proofpoint.event.collector.BatchProcessor.BatchHandler; +import com.proofpoint.event.collector.StaticEventTapConfig.FlowKey; import com.proofpoint.log.Logger; import com.proofpoint.testing.SerialScheduledExecutorService; import org.joda.time.DateTime; @@ -44,6 +45,10 @@ import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Strings.nullToEmpty; +import static com.proofpoint.event.collector.EventTapWriter.EVENT_TYPE_PROPERTY_NAME; +import static com.proofpoint.event.collector.EventTapWriter.FLOW_ID_PROPERTY_NAME; +import static com.proofpoint.event.collector.EventTapWriter.HTTP_PROPERTY_NAME; +import static com.proofpoint.event.collector.QosDelivery.RETRY; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -128,38 +133,38 @@ public void setup() eventTapWriter = new EventTapWriter( serviceSelector, executorService, batchProcessorFactory, eventTapFlowFactory, - eventTapConfig); + eventTapConfig, new StaticEventTapConfig()); eventTapWriter.start(); } @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "selector is null") public void testConstructorNullSelector() { - new EventTapWriter(null, executorService, batchProcessorFactory, eventTapFlowFactory, new EventTapConfig()); + new EventTapWriter(null, executorService, batchProcessorFactory, eventTapFlowFactory, new EventTapConfig(), new StaticEventTapConfig()); } @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "executorService is null") public void testConstructorNullExecutorService() { - new EventTapWriter(serviceSelector, null, batchProcessorFactory, eventTapFlowFactory, new EventTapConfig()); + new EventTapWriter(serviceSelector, null, batchProcessorFactory, eventTapFlowFactory, new EventTapConfig(), new StaticEventTapConfig()); } @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "batchProcessorFactory is null") public void testConstructorNullBatchProcessorFactory() { - new EventTapWriter(serviceSelector, executorService, null, eventTapFlowFactory, new EventTapConfig()); + new EventTapWriter(serviceSelector, executorService, null, eventTapFlowFactory, new EventTapConfig(), new StaticEventTapConfig()); } @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "eventTapFlowFactory is null") public void testConstructorNullEventTapFlowFactory() { - new EventTapWriter(serviceSelector, executorService, batchProcessorFactory, null, new EventTapConfig()); + new EventTapWriter(serviceSelector, executorService, batchProcessorFactory, null, new EventTapConfig(), new StaticEventTapConfig()); } @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "config is null") public void testConstructorNullConfig() { - new EventTapWriter(serviceSelector, executorService, batchProcessorFactory, eventTapFlowFactory, null); + new EventTapWriter(serviceSelector, executorService, batchProcessorFactory, eventTapFlowFactory, null, new StaticEventTapConfig()); } @Test @@ -785,12 +790,172 @@ public void testHttpOnlyTapsWithAllowHttpConsumersIsFalse() eventTapWriter = new EventTapWriter( serviceSelector, executorService, batchProcessorFactory, eventTapFlowFactory, - eventTapConfig); + eventTapConfig, new StaticEventTapConfig()); eventTapWriter.start(); updateThenRefreshFlowsThenCheck(ImmutableList.of(tapA, tapB), ImmutableList.of()); } + @Test + public void testStaticAnnouncementWithSingleUri() + { + FlowKey flowKey = extractFlowKeyFromTap(tapA1a); + String uri = extractUriFromTap(tapA1a); + + Map staticAnnouncements = ImmutableMap.of( + flowKey, + new PerFlowStaticEventTapConfig() + .setUris(uri) + ); + + StaticEventTapConfig staticEventTapConfig = new StaticEventTapConfig().setStaticTaps(staticAnnouncements); + eventTapWriter = new EventTapWriter( + serviceSelector, executorService, + batchProcessorFactory, eventTapFlowFactory, + eventTapConfig, staticEventTapConfig); + eventTapWriter.start(); + + writeEvents(eventsA[0], eventsB[0]); + forTap(tapA1a).verifyEvents(eventsA[0]); + } + + @Test + public void testStaticAnnouncementWithMultipleUris() + { + FlowKey flowKey = extractFlowKeyFromTap(tapA1); + + String uriA = extractUriFromTap(tapA1a); + String uriB = extractUriFromTap(tapA1b); + + Map staticAnnouncements = ImmutableMap.of( + flowKey, + new PerFlowStaticEventTapConfig() + .setUris(ImmutableSet.of(uriA, uriB)) + ); + + StaticEventTapConfig staticEventTapConfig = new StaticEventTapConfig().setStaticTaps(staticAnnouncements); + eventTapWriter = new EventTapWriter( + serviceSelector, executorService, + batchProcessorFactory, eventTapFlowFactory, + eventTapConfig, staticEventTapConfig); + eventTapWriter.start(); + + writeEvents(eventsA[0], eventsB[0]); + forSharedTaps(tapA1a, tapA1b).verifyEvents(eventsA[0]); + } + + @Test + public void testStaticAnnouncementWithQosEnabled() + { + FlowKey flowKey = extractFlowKeyFromTap(tapA1); + String uri = extractUriFromTap(tapA1); + + Map staticAnnouncements; + staticAnnouncements = ImmutableMap.of( + flowKey, + new PerFlowStaticEventTapConfig() + .setUris(uri) + .setQosDelivery(RETRY) + ); + + StaticEventTapConfig staticEventTapConfig = new StaticEventTapConfig().setStaticTaps(staticAnnouncements); + eventTapWriter = new EventTapWriter( + serviceSelector, executorService, + batchProcessorFactory, eventTapFlowFactory, + eventTapConfig, staticEventTapConfig); + eventTapWriter.start(); + + writeEvents(eventsA[0], eventsB[0]); + forTap(qtapA1).verifyEvents(eventsA[0]); + } + + @Test + public void testStaticAndDynamicAnnouncementForSameFlowIdSameEventType() + { + FlowKey flowKey = extractFlowKeyFromTap(tapA1a); + String uri = extractUriFromTap(tapA1a); + + Map staticAnnouncements = ImmutableMap.of( + flowKey, + new PerFlowStaticEventTapConfig() + .setUris(uri) + ); + + StaticEventTapConfig staticEventTapConfig = new StaticEventTapConfig().setStaticTaps(staticAnnouncements); + eventTapWriter = new EventTapWriter( + serviceSelector, executorService, + batchProcessorFactory, eventTapFlowFactory, + eventTapConfig, staticEventTapConfig); + eventTapWriter.start(); + + updateThenRefreshFlowsThenCheck(tapA1b); + + // When there's an overlap of (eventType, flowId) we expect the taps to be merged into one flow + writeEvents(eventsA[0], eventsB[0]); + forSharedTaps(tapA1a, tapA1b).verifyEvents(eventsA[0]); + } + + @Test + public void testStaticAndDynamicAnnouncementForSameFlowIdDifferentEventType() + { + FlowKey flowKey = extractFlowKeyFromTap(tapA1); + String uri = extractUriFromTap(tapA1); + + Map staticAnnouncements = ImmutableMap.of( + flowKey, + new PerFlowStaticEventTapConfig() + .setUris(uri) + ); + + StaticEventTapConfig staticEventTapConfig = new StaticEventTapConfig().setStaticTaps(staticAnnouncements); + eventTapWriter = new EventTapWriter( + serviceSelector, executorService, + batchProcessorFactory, eventTapFlowFactory, + eventTapConfig, staticEventTapConfig); + eventTapWriter.start(); + + updateThenRefreshFlowsThenCheck(ImmutableList.of(tapB1), ImmutableList.of(tapA1, tapB1)); + + writeEvents(eventsA[0], eventsB[0]); + forTap(tapA1).verifyEvents(eventsA[0]); + forTap(tapB1).verifyEvents(eventsB[0]); + } + + @Test + public void testStaticAndDynamicAnnouncementForDifferentFlowIdSameEventType() + { + FlowKey flowKey = extractFlowKeyFromTap(tapA1); + String uri = extractUriFromTap(tapA1); + + Map staticAnnouncements = ImmutableMap.of( + flowKey, + new PerFlowStaticEventTapConfig() + .setUris(uri) + ); + + StaticEventTapConfig staticEventTapConfig = new StaticEventTapConfig().setStaticTaps(staticAnnouncements); + eventTapWriter = new EventTapWriter( + serviceSelector, executorService, + batchProcessorFactory, eventTapFlowFactory, + eventTapConfig, staticEventTapConfig); + eventTapWriter.start(); + + updateThenRefreshFlowsThenCheck(ImmutableList.of(tapA2), ImmutableList.of(tapA1, tapA2)); + + writeEvents(eventsA[0], eventsB[0]); + forTap(tapA1).verifyEvents(eventsA[0]); + forTap(tapA2).verifyEvents(eventsA[0]); + } + + private static FlowKey extractFlowKeyFromTap(ServiceDescriptor tap) { + Map properties = tap.getProperties(); + return new FlowKey(properties.get(EVENT_TYPE_PROPERTY_NAME), properties.get(FLOW_ID_PROPERTY_NAME)); + } + + private static String extractUriFromTap(ServiceDescriptor tap) { + return tap.getProperties().get(HTTP_PROPERTY_NAME); + } + private void updateThenRefreshFlowsThenCheck(ServiceDescriptor... taps) { updateThenRefreshFlowsThenCheck(Arrays.asList(taps), Arrays.asList(taps)); @@ -936,7 +1101,7 @@ private EventTapFlowVerifier forSharedTaps(ServiceDescriptor... taps) for (ServiceDescriptor tap : taps) { String thisEventType = tap.getProperties().get("eventType"); - String thisFlowId = tap.getProperties().get(EventTapWriter.FLOW_ID_PROPERTY_NAME); + String thisFlowId = tap.getProperties().get(FLOW_ID_PROPERTY_NAME); String thisUri = tap.getProperties().get("https"); @@ -972,7 +1137,7 @@ private EventTapFlowVerifier forSharedTaps(ServiceDescriptor... taps) private static String extractProcessorName(ServiceDescriptor tap) { return format("%s{%s}", tap.getProperties().get("eventType"), - tap.getProperties().get(EventTapWriter.FLOW_ID_PROPERTY_NAME)); + tap.getProperties().get(FLOW_ID_PROPERTY_NAME)); } private static ServiceDescriptor createServiceDescriptor(String eventType, Map properties) @@ -982,15 +1147,12 @@ private static ServiceDescriptor createServiceDescriptor(String eventType, Mapof()); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = ImmutableMap.of( + "qos-delivery", "RETRY", + "uris", "http://1.2.3.40 , https://1.2.3.41:8333" + ); + + PerFlowStaticEventTapConfig expected = new PerFlowStaticEventTapConfig() + .setQosDelivery(RETRY) + .setUris(ImmutableSet.of("http://1.2.3.40", "https://1.2.3.41:8333")); + + assertFullMapping(properties, expected); + } + + @Test + public void testLegacyProperties() + { + Map properties = ImmutableMap.of( + "uris", URIS + ); + + assertLegacyEquivalence(PerFlowStaticEventTapConfig.class, properties); + } + + @Test + public void testInvalidUriStringFailsValidation() + { + PerFlowStaticEventTapConfig config = new PerFlowStaticEventTapConfig().setUris("http://1.2.3.4/path|dummy"); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy", ValidUri.class); + + config = new PerFlowStaticEventTapConfig().setUris(ImmutableSet.of("http://1.2.3.4/path|dummy", "http://1.2.3.5/path|dummy")); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy, http://1.2.3.5/path|dummy", ValidUri.class); + + config = new PerFlowStaticEventTapConfig().setUris(ImmutableSet.of("http://1.2.3.4/path", "http://1.2.3.4/path|dummy")); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy", ValidUri.class); + } + + @Test + public void testUriWithInvalidSchemeFailsValidation() + { + PerFlowStaticEventTapConfig config = new PerFlowStaticEventTapConfig().setUris("1.2.3.4"); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid scheme: 1.2.3.4", ValidUri.class); + + config = new PerFlowStaticEventTapConfig().setUris(ImmutableSet.of("1.2.3.4", "ftp://1.2.3.4")); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid scheme: 1.2.3.4, ftp://1.2.3.4", ValidUri.class); + + config = new PerFlowStaticEventTapConfig().setUris(ImmutableSet.of("http://1.2.3.4/path", "1.2.3.4")); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid scheme: 1.2.3.4", ValidUri.class); + } + + @Test + public void testUriWithInvalidSyntaxAndInvalidSchemeFailsValidation() + { + PerFlowStaticEventTapConfig config = new PerFlowStaticEventTapConfig().setUris(ImmutableSet.of("http://1.2.3.4/path|dummy", "ftp://1.2.3.4")); + assertFailsValidation(config, "uris", "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy; Invalid scheme: ftp://1.2.3.4", ValidUri.class); + } + + @Test + public void testEmptyURIsFailsValidation() + { + PerFlowStaticEventTapConfig config = new PerFlowStaticEventTapConfig(); + assertFailsValidation(config, "uris", "may not be empty", Size.class); + } + + @Test + public void testValidation() + { + PerFlowStaticEventTapConfig config = new PerFlowStaticEventTapConfig().setUris(URIS).setQosDelivery(RETRY); + assertValidates(config); + } +} diff --git a/src/test/java/com/proofpoint/event/collector/TestQosDelivery.java b/src/test/java/com/proofpoint/event/collector/TestQosDelivery.java new file mode 100644 index 0000000..4411dff --- /dev/null +++ b/src/test/java/com/proofpoint/event/collector/TestQosDelivery.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector; + +import org.testng.annotations.Test; + +import static com.proofpoint.event.collector.QosDelivery.BEST_EFFORT; +import static com.proofpoint.event.collector.QosDelivery.RETRY; +import static org.testng.Assert.assertEquals; + +public class TestQosDelivery +{ + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "No enum constant com.proofpoint.event.collector.QosDelivery.DUMMY") + public void testFailureFromStringIllegalValue() + { + QosDelivery.fromString("dummy"); + } + + @Test + public void testSuccessFromStringRetry() + { + assertEquals(QosDelivery.fromString("retry"), RETRY); + } + + @Test + public void tesSuccessFromStringBestEffort() + { + assertEquals(QosDelivery.fromString("bestEffort"), BEST_EFFORT); + } +} diff --git a/src/test/java/com/proofpoint/event/collector/TestStaticEventTapConfig.java b/src/test/java/com/proofpoint/event/collector/TestStaticEventTapConfig.java new file mode 100644 index 0000000..5226de3 --- /dev/null +++ b/src/test/java/com/proofpoint/event/collector/TestStaticEventTapConfig.java @@ -0,0 +1,141 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector; + +import com.google.common.collect.ImmutableMap; +import com.proofpoint.event.collector.StaticEventTapConfig.FlowKey; +import org.testng.annotations.Test; + +import javax.validation.constraints.Size; +import java.util.Map; + +import static com.proofpoint.configuration.testing.ConfigAssertions.assertFullMapping; +import static com.proofpoint.configuration.testing.ConfigAssertions.assertLegacyEquivalence; +import static com.proofpoint.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static com.proofpoint.configuration.testing.ConfigAssertions.recordDefaults; +import static com.proofpoint.testing.ValidationAssertions.assertFailsValidation; +import static com.proofpoint.testing.ValidationAssertions.assertValidates; +import static org.testng.Assert.assertEquals; + +public class TestStaticEventTapConfig +{ + private static final String URIS = "http://1.2.3.40 , http://1.2.3.41"; + private static final String URIS_A = URIS; + private static final String URIS_B = "http://1.2.3.50 , http://1.2.3.51"; + + @Test + public void testDefaults() + { + assertRecordedDefaults( + recordDefaults(StaticEventTapConfig.class) + .setStaticTaps(ImmutableMap.of()) + ); + } + + @Test + public void testExplicitPropertyMappings() + { + ImmutableMap.Builder properties = ImmutableMap.builder(); + properties.put("static-taps.typeA@foo.uris", URIS_A); + properties.put("static-taps.typeA@foo.qos-delivery", "retry"); + + properties.put("static-taps.typeA@bar.uris", URIS_B); + properties.put("static-taps.typeA@bar.qos-delivery", "retry"); + + Map inputMap = ImmutableMap.of( + FlowKey.valueOf("typeA@foo"), + new PerFlowStaticEventTapConfig() + .setQosDelivery(QosDelivery.RETRY) + .setUris(URIS_A), + + FlowKey.valueOf("typeA@bar"), + new PerFlowStaticEventTapConfig() + .setQosDelivery(QosDelivery.RETRY) + .setUris(URIS_B) + ); + + assertFullMapping(properties.build(), new StaticEventTapConfig().setStaticTaps(inputMap)); + } + + @Test + public void testLegacyProperties() + { + assertLegacyEquivalence(StaticEventTapConfig.class, ImmutableMap.of()); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Invalid flow key: @foo; Elements separate by @ cannot be empty") + public void testFlowKeyConstructorMissingEventTypeThrowsException() + { + FlowKey.valueOf("@foo"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Invalid flow key: typeA@; Elements separate by @ cannot be empty") + public void testFlowKeyConstructorMissingFlowIdThrowsException() + { + FlowKey.valueOf("typeA@"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Invalid flow key: @; Elements separate by @ cannot be empty") + public void testFlowKeyConstructorMissingEventTypeAndFlowIdThrowsException() + { + FlowKey.valueOf("@"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Invalid flow key: typeA_foo; Flow key must have a single @ character") + public void testFlowKeyConstructorInvalidKeySyntaxThrowsException() + { + FlowKey.valueOf("typeA_foo"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Invalid flow key: typeA@@@@@foo; Flow key must have a single @ character") + public void testFlowKeyConstructorInvalidKeySyntax2ThrowsException() + { + FlowKey.valueOf("typeA@@@@@foo"); + } + + @Test + public void testFlowKeyConstructorValid() + { + assertEquals(FlowKey.valueOf("typeA@foo"), new FlowKey("typeA", "foo")); + } + + @Test + public void testInvalidPerFlowStaticEventTapConfigFailsValidation() + { + Map inputMap = ImmutableMap.of( + new FlowKey("typeA", "foo"), + new PerFlowStaticEventTapConfig() + .setUris("") + ); + + StaticEventTapConfig config = new StaticEventTapConfig().setStaticTaps(inputMap); + assertFailsValidation(config, "staticTaps[FlowKey{eventType=typeA, flowId=foo}].uris", "may not be empty", Size.class); + } + + @Test + public void testValidation() + { + Map inputMap = ImmutableMap.of( + FlowKey.valueOf("typeA@foo"), + new PerFlowStaticEventTapConfig() + .setQosDelivery(QosDelivery.RETRY) + .setUris(URIS) + ); + + StaticEventTapConfig config = new StaticEventTapConfig().setStaticTaps(inputMap); + assertValidates(config); + } +} diff --git a/src/test/java/com/proofpoint/event/collector/validation/TestValidUriValidator.java b/src/test/java/com/proofpoint/event/collector/validation/TestValidUriValidator.java new file mode 100644 index 0000000..e5eb741 --- /dev/null +++ b/src/test/java/com/proofpoint/event/collector/validation/TestValidUriValidator.java @@ -0,0 +1,81 @@ +/* + * Copyright 2011-2014 Proofpoint, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.proofpoint.event.collector.validation; + +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import static com.proofpoint.testing.ValidationAssertions.assertFailsValidation; +import static com.proofpoint.testing.ValidationAssertions.assertValidates; + +public class TestValidUriValidator +{ + @Test + public void testAssertValidates() + { + assertValidates(new IterableStringBean(ImmutableList.of("http://www.example.com"))); + assertValidates(new IterableStringBean(ImmutableList.of("http://www.example.com", "https://www.example2.com"))); + } + + @Test + public void testAssertEmptyIterableValidates() + { + assertValidates(new IterableStringBean(ImmutableList.of())); + } + + @Test + public void testFailsSyntaxValidation() + { + assertFailsUriValidation(ImmutableList.of("http://1.2.3.4/path|dummy"), "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy"); + assertFailsUriValidation(ImmutableList.of("http://1.2.3.4/path|dummy", "http://1.2.3.5/path|dummy"), "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy, http://1.2.3.5/path|dummy"); + assertFailsUriValidation(ImmutableList.of("http://1.2.3.4/path", "http://1.2.3.4/path|dummy"), "Invalid URIs: Invalid syntax: http://1.2.3.4/path|dummy"); + } + + @Test + public void testFailsSchemeValidation() + { + assertFailsUriValidation(ImmutableList.of("ftp://1.2.3.4/path"), "Invalid URIs: Invalid scheme: ftp://1.2.3.4/path"); + assertFailsUriValidation(ImmutableList.of("ftp://1.2.3.4/path", "ftp://1.2.3.5/path"), "Invalid URIs: Invalid scheme: ftp://1.2.3.4/path, ftp://1.2.3.5/path"); + assertFailsUriValidation(ImmutableList.of("ftp://1.2.3.4/path", "http://1.2.3.4/valid"), "Invalid URIs: Invalid scheme: ftp://1.2.3.4/path"); + } + + @Test + public void testFailsSyntaxAndSchemeValidation() + { + assertFailsUriValidation(ImmutableList.of("ftp://1.2.3.4/path|dummy"), "Invalid URIs: Invalid syntax: ftp://1.2.3.4/path|dummy"); + } + + public void assertFailsUriValidation(Iterable values, String expectedErrorMessage) + { + assertFailsValidation(new IterableStringBean(values), "values", expectedErrorMessage, ValidUri.class); + } + + public static class IterableStringBean + { + private Iterable values; + + private IterableStringBean(Iterable values) + { + this.values = values; + } + + @ValidUri(schemes = {"http", "https"}) + public Iterable getValues() + { + return values; + } + } +} \ No newline at end of file