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.util;
018
019import java.io.ByteArrayOutputStream;
020import java.io.Closeable;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.Iterator;
024import java.util.Scanner;
025import java.util.concurrent.atomic.AtomicBoolean;
026
027import org.apache.camel.CamelContext;
028import org.apache.camel.Exchange;
029import org.apache.camel.NoTypeConversionAvailableException;
030
031/**
032 * Group based {@link Iterator} which groups the given {@link Iterator} a number of times
033 * and then return a combined response as a String.
034 * <p/>
035 * This implementation uses an internal byte array buffer, to combine the response.
036 * The token is inserted between the individual parts.
037 * <p/>
038 * For example if you group by new line, then a new line token is inserted between the lines.
039 */
040public final class GroupTokenIterator implements Iterator<Object>, Closeable {
041
042    private final CamelContext camelContext;
043    private final Exchange exchange;
044    private final Iterator<?> it;
045    private final String token;
046    private final int group;
047    private final boolean skipFirst;
048    private final AtomicBoolean hasSkipFirst;
049    private boolean closed;
050    private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
051    
052    /**
053     * Creates a new token based group iterator
054     *
055     * @param camelContext  the camel context
056     * @param it            the iterator to group
057     * @param token         then token used to separate between the parts, use <tt>null</tt> to not add the token
058     * @param group         number of parts to group together
059     * @throws IllegalArgumentException is thrown if group is not a positive number
060     * @deprecated  Please use GroupIterator(Exchange exchange, Iterator<?> it, String token, int group) instead
061     */
062    @Deprecated 
063    public GroupTokenIterator(CamelContext camelContext, Iterator<?> it, String token, int group) {
064        this.exchange = null;
065        this.camelContext = camelContext;
066        this.it = it;
067        this.token = token;
068        this.group = group;
069        if (group <= 0) {
070            throw new IllegalArgumentException("Group must be a positive number, was: " + group);
071        }
072        this.skipFirst = false;
073        this.hasSkipFirst = null;
074    }
075
076    /**
077     * Creates a new token based group iterator
078     *
079     * @param exchange      the exchange used to create this group iterator
080     * @param it            the iterator to group
081     * @param token         then token used to separate between the parts, use <tt>null</tt> to not add the token
082     * @param group         number of parts to group together
083     * @throws IllegalArgumentException is thrown if group is not a positive number
084     */
085    public GroupTokenIterator(Exchange exchange, Iterator<?> it, String token, int group, boolean skipFirst) {
086        this.exchange = exchange;
087        this.camelContext = exchange.getContext();
088        this.it = it;
089        this.token = token;
090        this.group = group;
091        if (group <= 0) {
092            throw new IllegalArgumentException("Group must be a positive number, was: " + group);
093        }
094        this.skipFirst = skipFirst;
095        if (skipFirst) {
096            this.hasSkipFirst = new AtomicBoolean();
097        } else {
098            this.hasSkipFirst = null;
099        }
100    }
101
102    @Override
103    public void close() throws IOException {
104        try {
105            if (it instanceof Scanner) {
106                // special for Scanner which implement the Closeable since JDK7 
107                Scanner scanner = (Scanner) it;
108                scanner.close();
109                IOException ioException = scanner.ioException();
110                if (ioException != null) {
111                    throw ioException;
112                }
113            } else if (it instanceof Closeable) {
114                IOHelper.closeWithException((Closeable) it);
115            }
116        } finally {
117            // close the buffer as well
118            bos.close();
119            // we are now closed
120            closed = true;
121        }
122    }
123
124    @Override
125    public boolean hasNext() {
126        if (closed) {
127            return false;
128        }
129
130        boolean answer = it.hasNext();
131        if (!answer) {
132            // auto close
133            try {
134                close();
135            } catch (IOException e) {
136                // ignore
137            }
138        }
139        return answer;
140    }
141
142    @Override
143    public Object next() {
144        try {
145            return doNext();
146        } catch (Exception e) {
147            throw ObjectHelper.wrapRuntimeCamelException(e);
148        }
149    }
150
151    private Object doNext() throws IOException, NoTypeConversionAvailableException {
152        int count = 0;
153        Object data = "";
154        while (count < group && it.hasNext()) {
155            data = it.next();
156
157            if (skipFirst && hasSkipFirst.compareAndSet(false, true)) {
158                if (it.hasNext()) {
159                    data = it.next();
160                } else {
161                    // Content with header only which is marked to skip
162                    data = "";
163                }
164            }
165
166            // include token in between
167            if (data != null && count > 0 && token != null) {
168                bos.write(token.getBytes());
169            }
170            if (data instanceof InputStream) {
171                InputStream is = (InputStream) data;
172                IOHelper.copy(is, bos);
173            } else if (data instanceof byte[]) {
174                byte[] bytes = (byte[]) data;
175                bos.write(bytes);
176            } else if (data != null) {
177                // convert to input stream
178                InputStream is = camelContext.getTypeConverter().mandatoryConvertTo(InputStream.class, exchange, data);
179                IOHelper.copy(is, bos);
180            }
181
182            count++;
183        }
184
185        // prepare and return answer as String using exchange's charset
186        String answer = bos.toString(IOHelper.getCharsetName(exchange));
187        bos.reset();
188        return answer;
189    }
190
191    @Override
192    public void remove() {
193        it.remove();
194    }
195}