001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.component.dataset;
018
019import java.util.concurrent.atomic.AtomicInteger;
020
021import org.apache.camel.Component;
022import org.apache.camel.Consumer;
023import org.apache.camel.Exchange;
024import org.apache.camel.Message;
025import org.apache.camel.Processor;
026import org.apache.camel.Producer;
027import org.apache.camel.Service;
028import org.apache.camel.component.mock.MockEndpoint;
029import org.apache.camel.processor.ThroughputLogger;
030import org.apache.camel.spi.Metadata;
031import org.apache.camel.spi.UriEndpoint;
032import org.apache.camel.spi.UriParam;
033import org.apache.camel.spi.UriPath;
034import org.apache.camel.util.CamelLogger;
035import org.apache.camel.util.ExchangeHelper;
036import org.apache.camel.util.ObjectHelper;
037import org.apache.camel.util.URISupport;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * The dataset component provides a mechanism to easily perform load & soak testing of your system.
043 *
044 * It works by allowing you to create DataSet instances both as a source of messages and as a way to assert that the data set is received.
045 * Camel will use the throughput logger when sending dataset's.
046 */
047@UriEndpoint(firstVersion = "1.3.0", scheme = "dataset", title = "Dataset", syntax = "dataset:name",
048    consumerClass = DataSetConsumer.class, label = "core,testing", lenientProperties = true)
049public class DataSetEndpoint extends MockEndpoint implements Service {
050    private final transient Logger log;
051    private final AtomicInteger receivedCounter = new AtomicInteger();
052    @UriPath(name = "name", description = "Name of DataSet to lookup in the registry") @Metadata(required = "true")
053    private volatile DataSet dataSet;
054    @UriParam(label = "consumer", defaultValue = "0")
055    private int minRate;
056    @UriParam(label = "consumer", defaultValue = "3")
057    private long produceDelay = 3;
058    @UriParam(label = "producer", defaultValue = "0")
059    private long consumeDelay;
060    @UriParam(label = "consumer", defaultValue = "0")
061    private long preloadSize;
062    @UriParam(label = "consumer", defaultValue = "1000")
063    private long initialDelay = 1000;
064    @UriParam(enums = "strict,lenient,off", defaultValue = "lenient")
065    private String dataSetIndex = "lenient";
066
067    @Deprecated
068    public DataSetEndpoint() {
069        this.log = LoggerFactory.getLogger(DataSetEndpoint.class);
070        // optimize as we dont need to copy the exchange
071        setCopyOnExchange(false);
072    }
073
074    public DataSetEndpoint(String endpointUri, Component component, DataSet dataSet) {
075        super(endpointUri, component);
076        this.dataSet = dataSet;
077        this.log = LoggerFactory.getLogger(endpointUri);
078        // optimize as we dont need to copy the exchange
079        setCopyOnExchange(false);
080    }
081
082    public static void assertEquals(String description, Object expected, Object actual, Exchange exchange) {
083        if (!ObjectHelper.equal(expected, actual)) {
084            throw new AssertionError(description + " does not match. Expected: " + expected + " but was: " + actual + " on " + exchange + " with headers: " + exchange.getIn().getHeaders());
085        }
086    }
087
088    @Override
089    public Consumer createConsumer(Processor processor) throws Exception {
090        Consumer answer = new DataSetConsumer(this, processor);
091        configureConsumer(answer);
092        return answer;
093    }
094
095    @Override
096    public Producer createProducer() throws Exception {
097        Producer answer = super.createProducer();
098
099        long size = getDataSet().getSize();
100        expectedMessageCount((int) size);
101
102        return answer;
103    }
104
105    @Override
106    public void reset() {
107        super.reset();
108        receivedCounter.set(0);
109    }
110
111    @Override
112    public int getReceivedCounter() {
113        return receivedCounter.get();
114    }
115
116    /**
117     * Creates a message exchange for the given index in the {@link DataSet}
118     */
119    public Exchange createExchange(long messageIndex) throws Exception {
120        Exchange exchange = createExchange();
121
122        getDataSet().populateMessage(exchange, messageIndex);
123
124        if (!getDataSetIndex().equals("off")) {
125            Message in = exchange.getIn();
126            in.setHeader(Exchange.DATASET_INDEX, messageIndex);
127        }
128
129        return exchange;
130    }
131
132    @Override
133    protected void waitForCompleteLatch(long timeout) throws InterruptedException {
134        super.waitForCompleteLatch(timeout);
135
136        if (minRate > 0) {
137            int count = getReceivedCounter();
138            do {
139                // wait as long as we get a decent message rate
140                super.waitForCompleteLatch(1000L);
141                count = getReceivedCounter() - count;
142            } while (count >= minRate);
143        }
144    }
145
146    // Properties
147    //-------------------------------------------------------------------------
148
149    public DataSet getDataSet() {
150        return dataSet;
151    }
152
153    public void setDataSet(DataSet dataSet) {
154        this.dataSet = dataSet;
155    }
156
157    public int getMinRate() {
158        return minRate;
159    }
160
161    /**
162     * Wait until the DataSet contains at least this number of messages
163     */
164    public void setMinRate(int minRate) {
165        this.minRate = minRate;
166    }
167
168    public long getPreloadSize() {
169        return preloadSize;
170    }
171
172    /**
173     * Sets how many messages should be preloaded (sent) before the route completes its initialization
174     */
175    public void setPreloadSize(long preloadSize) {
176        this.preloadSize = preloadSize;
177    }
178
179    public long getConsumeDelay() {
180        return consumeDelay;
181    }
182
183    /**
184     * Allows a delay to be specified which causes a delay when a message is consumed by the producer (to simulate slow processing)
185     */
186    public void setConsumeDelay(long consumeDelay) {
187        this.consumeDelay = consumeDelay;
188    }
189
190    public long getProduceDelay() {
191        return produceDelay;
192    }
193
194    /**
195     * Allows a delay to be specified which causes a delay when a message is sent by the consumer (to simulate slow processing)
196     */
197    public void setProduceDelay(long produceDelay) {
198        this.produceDelay = produceDelay;
199    }
200
201    public long getInitialDelay() {
202        return initialDelay;
203    }
204
205    /**
206     * Time period in millis to wait before starting sending messages.
207     */
208    public void setInitialDelay(long initialDelay) {
209        this.initialDelay = initialDelay;
210    }
211
212    /**
213     * Controls the behaviour of the CamelDataSetIndex header.
214     * For Consumers:
215     * - off => the header will not be set
216     * - strict/lenient => the header will be set
217     * For Producers:
218     * - off => the header value will not be verified, and will not be set if it is not present
219     * = strict => the header value must be present and will be verified
220     * = lenient => the header value will be verified if it is present, and will be set if it is not present
221     */
222    public void setDataSetIndex(String dataSetIndex) {
223        switch (dataSetIndex) {
224        case "off":
225        case "lenient":
226        case "strict":
227            this.dataSetIndex = dataSetIndex;
228            break;
229        default:
230            throw new IllegalArgumentException("Invalid value specified for the dataSetIndex URI parameter:" + dataSetIndex
231                    + "Supported values are strict, lenient and off ");
232        }
233    }
234
235    public String getDataSetIndex() {
236        return dataSetIndex;
237    }
238
239    // Implementation methods
240    //-------------------------------------------------------------------------
241
242    @Override
243    protected void performAssertions(Exchange actual, Exchange copy) throws Exception {
244        int receivedCount = receivedCounter.incrementAndGet();
245        long index = receivedCount - 1;
246        Exchange expected = createExchange(index);
247
248        // now let's assert that they are the same
249        if (log.isDebugEnabled()) {
250            if (copy.getIn().getHeader(Exchange.DATASET_INDEX) != null) {
251                log.debug("Received message: {} (DataSet index={}) = {}",
252                        new Object[]{index, copy.getIn().getHeader(Exchange.DATASET_INDEX, Integer.class), copy});
253            } else {
254                log.debug("Received message: {} = {}",
255                        new Object[]{index, copy});
256            }
257        }
258
259        assertMessageExpected(index, expected, copy);
260
261        if (consumeDelay > 0) {
262            Thread.sleep(consumeDelay);
263        }
264    }
265
266    protected void assertMessageExpected(long index, Exchange expected, Exchange actual) throws Exception {
267        switch (getDataSetIndex()) {
268        case "off":
269            break;
270        case "strict":
271            long actualCounter = ExchangeHelper.getMandatoryHeader(actual, Exchange.DATASET_INDEX, Long.class);
272            assertEquals("Header: " + Exchange.DATASET_INDEX, index, actualCounter, actual);
273            break;
274        case "lenient":
275        default:
276            // Validate the header value if it is present
277            Long dataSetIndexHeaderValue = actual.getIn().getHeader(Exchange.DATASET_INDEX, Long.class);
278            if (dataSetIndexHeaderValue != null) {
279                assertEquals("Header: " + Exchange.DATASET_INDEX, index, dataSetIndexHeaderValue, actual);
280            } else {
281                // set the header if it isn't there
282                actual.getIn().setHeader(Exchange.DATASET_INDEX, index);
283            }
284            break;
285        }
286
287        getDataSet().assertMessageExpected(this, expected, actual, index);
288    }
289
290    protected ThroughputLogger createReporter() {
291        // must sanitize uri to avoid logging sensitive information
292        String uri = URISupport.sanitizeUri(getEndpointUri());
293        CamelLogger logger = new CamelLogger(uri);
294        ThroughputLogger answer = new ThroughputLogger(logger, (int) this.getDataSet().getReportCount());
295        answer.setAction("Received");
296        return answer;
297    }
298
299    @Override
300    protected void doStart() throws Exception {
301        super.doStart();
302
303        if (reporter == null) {
304            reporter = createReporter();
305        }
306
307        log.info(this + " expecting " + getExpectedCount() + " messages");
308    }
309
310}