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
093        // expectedMessageCount((int) size);
094
095        return answer;
096    }
097
098    @Override
099    public Producer createProducer() throws Exception {
100        Producer answer = super.createProducer();
101
102        long size = getDataSet().getSize();
103        expectedMessageCount((int) size);
104
105        return answer;
106    }
107
108    @Override
109    public void reset() {
110        super.reset();
111        receivedCounter.set(0);
112    }
113
114    @Override
115    public int getReceivedCounter() {
116        return receivedCounter.get();
117    }
118
119    /**
120     * Creates a message exchange for the given index in the {@link DataSet}
121     */
122    public Exchange createExchange(long messageIndex) throws Exception {
123        Exchange exchange = createExchange();
124
125        getDataSet().populateMessage(exchange, messageIndex);
126
127        if (!getDataSetIndex().equals("off")) {
128            Message in = exchange.getIn();
129            in.setHeader(Exchange.DATASET_INDEX, messageIndex);
130        }
131
132        return exchange;
133    }
134
135    @Override
136    protected void waitForCompleteLatch(long timeout) throws InterruptedException {
137        super.waitForCompleteLatch(timeout);
138
139        if (minRate > 0) {
140            int count = getReceivedCounter();
141            do {
142                // wait as long as we get a decent message rate
143                super.waitForCompleteLatch(1000L);
144                count = getReceivedCounter() - count;
145            } while (count >= minRate);
146        }
147    }
148
149    // Properties
150    //-------------------------------------------------------------------------
151
152    public DataSet getDataSet() {
153        return dataSet;
154    }
155
156    public void setDataSet(DataSet dataSet) {
157        this.dataSet = dataSet;
158    }
159
160    public int getMinRate() {
161        return minRate;
162    }
163
164    /**
165     * Wait until the DataSet contains at least this number of messages
166     */
167    public void setMinRate(int minRate) {
168        this.minRate = minRate;
169    }
170
171    public long getPreloadSize() {
172        return preloadSize;
173    }
174
175    /**
176     * Sets how many messages should be preloaded (sent) before the route completes its initialization
177     */
178    public void setPreloadSize(long preloadSize) {
179        this.preloadSize = preloadSize;
180    }
181
182    public long getConsumeDelay() {
183        return consumeDelay;
184    }
185
186    /**
187     * Allows a delay to be specified which causes a delay when a message is consumed by the producer (to simulate slow processing)
188     */
189    public void setConsumeDelay(long consumeDelay) {
190        this.consumeDelay = consumeDelay;
191    }
192
193    public long getProduceDelay() {
194        return produceDelay;
195    }
196
197    /**
198     * Allows a delay to be specified which causes a delay when a message is sent by the consumer (to simulate slow processing)
199     */
200    public void setProduceDelay(long produceDelay) {
201        this.produceDelay = produceDelay;
202    }
203
204    public long getInitialDelay() {
205        return initialDelay;
206    }
207
208    /**
209     * Time period in millis to wait before starting sending messages.
210     */
211    public void setInitialDelay(long initialDelay) {
212        this.initialDelay = initialDelay;
213    }
214
215    /**
216     * Controls the behaviour of the CamelDataSetIndex header.
217     * For Consumers:
218     * - off => the header will not be set
219     * - strict/lenient => the header will be set
220     * For Producers:
221     * - off => the header value will not be verified, and will not be set if it is not present
222     * = strict => the header value must be present and will be verified
223     * = lenient => the header value will be verified if it is present, and will be set if it is not present
224     */
225    public void setDataSetIndex(String dataSetIndex) {
226        switch (dataSetIndex) {
227        case "off":
228        case "lenient":
229        case "strict":
230            this.dataSetIndex = dataSetIndex;
231            break;
232        default:
233            throw new IllegalArgumentException("Invalid value specified for the dataSetIndex URI parameter:" + dataSetIndex
234                    + "Supported values are strict, lenient and off ");
235        }
236    }
237
238    public String getDataSetIndex() {
239        return dataSetIndex;
240    }
241
242    // Implementation methods
243    //-------------------------------------------------------------------------
244
245    @Override
246    protected void performAssertions(Exchange actual, Exchange copy) throws Exception {
247        int receivedCount = receivedCounter.incrementAndGet();
248        long index = receivedCount - 1;
249        Exchange expected = createExchange(index);
250
251        // now let's assert that they are the same
252        if (log.isDebugEnabled()) {
253            if (copy.getIn().getHeader(Exchange.DATASET_INDEX) != null) {
254                log.debug("Received message: {} (DataSet index={}) = {}",
255                        new Object[]{index, copy.getIn().getHeader(Exchange.DATASET_INDEX, Integer.class), copy});
256            } else {
257                log.debug("Received message: {} = {}",
258                        new Object[]{index, copy});
259            }
260        }
261
262        assertMessageExpected(index, expected, copy);
263
264        if (consumeDelay > 0) {
265            Thread.sleep(consumeDelay);
266        }
267    }
268
269    protected void assertMessageExpected(long index, Exchange expected, Exchange actual) throws Exception {
270        switch (getDataSetIndex()) {
271        case "off":
272            break;
273        case "strict":
274            long actualCounter = ExchangeHelper.getMandatoryHeader(actual, Exchange.DATASET_INDEX, Long.class);
275            assertEquals("Header: " + Exchange.DATASET_INDEX, index, actualCounter, actual);
276            break;
277        case "lenient":
278        default:
279            // Validate the header value if it is present
280            Long dataSetIndexHeaderValue = actual.getIn().getHeader(Exchange.DATASET_INDEX, Long.class);
281            if (dataSetIndexHeaderValue != null) {
282                assertEquals("Header: " + Exchange.DATASET_INDEX, index, dataSetIndexHeaderValue, actual);
283            } else {
284                // set the header if it isn't there
285                actual.getIn().setHeader(Exchange.DATASET_INDEX, index);
286            }
287            break;
288        }
289
290        getDataSet().assertMessageExpected(this, expected, actual, index);
291    }
292
293    protected ThroughputLogger createReporter() {
294        // must sanitize uri to avoid logging sensitive information
295        String uri = URISupport.sanitizeUri(getEndpointUri());
296        CamelLogger logger = new CamelLogger(uri);
297        ThroughputLogger answer = new ThroughputLogger(logger, (int) this.getDataSet().getReportCount());
298        answer.setAction("Received");
299        return answer;
300    }
301
302    @Override
303    protected void doStart() throws Exception {
304        super.doStart();
305
306        if (reporter == null) {
307            reporter = createReporter();
308        }
309
310        log.info(this + " expecting " + getExpectedCount() + " messages");
311    }
312
313}