blob: f7468491e89e27101c64c03d0beba0c1647a4740 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.olingo.client.core.communication.request.batch;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.olingo.client.api.ODataBatchConstants;
import org.apache.olingo.client.api.communication.header.HeaderName;
import org.apache.olingo.client.api.communication.request.ODataStreamer;
import org.apache.olingo.client.api.communication.request.batch.ODataBatchLineIterator;
import org.apache.olingo.commons.api.Constants;
import org.apache.olingo.commons.api.format.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class for batch requests and responses.
*/
public class ODataBatchUtilities {
public static enum BatchItemType {
NONE,
CHANGESET,
RETRIEVE
}
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(ODataBatchUtilities.class);
/**
* Response line syntax.
*/
private static final Pattern RESPONSE_PATTERN =
Pattern.compile("HTTP/\\d\\.\\d (\\d+) (.*)", Pattern.CASE_INSENSITIVE);
/**
* Reads batch part taking source and delimiter (boundary) from given batch controller.
* <p>
* Usually used to consume/discard useless lines.
*
* @param batchController batch controller.
* @param checkCurrent if 'TRUE' the current line will be included into the delimiter verification.
* @return latest read line.
*/
public static String readBatchPart(final ODataBatchController batchController, final boolean checkCurrent) {
return readBatchPart(batchController, null, -1, checkCurrent);
}
/**
* Reads the given number of line from the given batch wrapped into the batch controller.
* <p>
* Usually used to consume/discard useless lines.
*
* @param batchController batch controller.
* @param count number of batch line to be read.
* @return latest read line.
*/
public static String readBatchPart(final ODataBatchController batchController, final int count) {
return readBatchPart(batchController, null, count, true);
}
/**
* Reads batch part taking source and delimiter (boundary) from given batch controller.
* <p>
* Usually used to read an entire batch part.
*
* @param controller batch controller.
* @param os destination stream of batch part (null to discard).
* @param checkCurrent if 'TRUE' the current line will be included into the delimiter verification.
* @return latest read line.
*/
public static String readBatchPart(
final ODataBatchController controller, final OutputStream os, final boolean checkCurrent) {
return readBatchPart(controller, os, -1, checkCurrent);
}
/**
* Reads batch part taking source and delimiter (boundary) from given batch controller.
* <p>
* Usually used to read an entire batch part.
*
* @param controller batch controller.
* @param os destination stream of batch part (null to discard).
* @param count number of batch line to be read.
* @param checkCurrent if 'TRUE' the current line will be included into the delimiter verification.
* @return latest read line.
*/
public static String readBatchPart(
final ODataBatchController controller, final OutputStream os, final int count, final boolean checkCurrent) {
String currentLine;
synchronized (controller.getBatchLineIterator()) {
currentLine = checkCurrent ? controller.getBatchLineIterator().getCurrent() : null;
if (count < 0) {
try {
boolean notEndLine = isNotEndLine(controller, currentLine);
while (controller.isValidBatch() && notEndLine && controller.getBatchLineIterator().hasNext()) {
currentLine = controller.getBatchLineIterator().nextLine();
LOG.debug("Read line '{}' (end-line '{}')", currentLine, controller.getBoundary());
notEndLine = isNotEndLine(controller, currentLine);
if (notEndLine && os != null) {
os.write(currentLine.getBytes(Constants.UTF8));
os.write(ODataStreamer.CRLF);
}
}
} catch (IOException e) {
LOG.error("Error reading batch part", e);
throw new IllegalStateException(e);
}
} else {
for (int i = 0; controller.isValidBatch() && controller.getBatchLineIterator().hasNext() && i < count; i++) {
currentLine = controller.getBatchLineIterator().nextLine();
}
}
}
return currentLine;
}
/**
* Reads headers from the batch starting from the given position.
*
* @param iterator batch iterator.
* @return Map of header name in header values.
*/
public static Map<String, Collection<String>> readHeaders(final ODataBatchLineIterator iterator) {
final Map<String, Collection<String>> target =
new TreeMap<String, Collection<String>>(String.CASE_INSENSITIVE_ORDER);
readHeaders(iterator, target);
return target;
}
/**
* Reads headers from the batch starting from the given position.
* <p>
* Retrieved headers will be added to the map given by target parameter.
*
* @param iterator batch iterator.
* @param target destination of the retrieved headers.
*/
public static void readHeaders(
final ODataBatchLineIterator iterator, final Map<String, Collection<String>> target) {
try {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
readBatchPart(new ODataBatchController(iterator, null), baos, true);
final LineIterator headers = IOUtils.lineIterator(new ByteArrayInputStream(baos.toByteArray()), Constants.UTF8);
while (headers.hasNext()) {
final String line = headers.nextLine().trim();
if (StringUtils.isNotBlank(line)) {
addHeaderLine(line, target);
}
}
} catch (Exception e) {
LOG.error("Error retrieving headers", e);
throw new IllegalStateException(e);
}
}
/**
* Parses and adds the given header line to the given target map.
*
* @param headerLine header line to be added.
* @param targetMap target map.
*/
public static void addHeaderLine(final String headerLine, final Map<String, Collection<String>> targetMap) {
final int sep = headerLine.indexOf(':');
if (sep > 0 && sep < headerLine.length() - 1) {
final String key = headerLine.substring(0, sep).trim();
final Collection<String> value;
if (targetMap.containsKey(key)) {
value = targetMap.get(key);
} else {
value = new HashSet<String>();
targetMap.put(key, value);
}
value.add(headerLine.substring(sep + 1, headerLine.length()).trim());
}
}
/**
* Retrieved batch boundary from the given content-type header values.
*
* @param contentType content-types.
* @return batch boundary.
*/
public static String getBoundaryFromHeader(final Collection<String> contentType) {
final String boundaryKey = ODataBatchConstants.BOUNDARY + "=";
if (contentType == null || contentType.isEmpty() || !contentType.toString().contains(boundaryKey)) {
throw new IllegalArgumentException("Invalid content type");
}
final String headerValue = contentType.toString();
final int start = headerValue.indexOf(boundaryKey) + boundaryKey.length();
int end = headerValue.indexOf(';', start);
if (end < 0) {
end = headerValue.indexOf(']', start);
}
final String res = headerValue.substring(start, end);
return res.startsWith("--") ? res : "--" + res;
}
/**
* Retrieves response line from the given position.
*
* @param iterator batch iterator.
* @return retrieved response line.
*/
public static Map.Entry<Integer, String> readResponseLine(final ODataBatchLineIterator iterator) {
final String line = readBatchPart(new ODataBatchController(iterator, null), 1);
LOG.debug("Response line '{}'", line);
final Matcher matcher = RESPONSE_PATTERN.matcher(line.trim());
if (matcher.matches()) {
return new AbstractMap.SimpleEntry<Integer, String>(Integer.valueOf(matcher.group(1)), matcher.group(2));
}
throw new IllegalArgumentException("Invalid response line '" + line + "'");
}
/**
* Retrieves headers of the next batch item.
*
* @param iterator batch line iterator.
* @param boundary batch boundary.
* @return batch item headers.
*/
public static Map<String, Collection<String>> nextItemHeaders(
final ODataBatchLineIterator iterator, final String boundary) {
final Map<String, Collection<String>> headers =
new TreeMap<String, Collection<String>>(String.CASE_INSENSITIVE_ORDER);
final String line = ODataBatchUtilities.readBatchPart(new ODataBatchController(iterator, boundary), true);
if (line != null && line.trim().equals(boundary)) {
ODataBatchUtilities.readHeaders(iterator, headers);
}
LOG.debug("Retrieved batch item headers {}", headers);
return headers;
}
/**
* Retrieves item type from item headers.
*
* @param headers batch item headers.
* @return batch item type.
*/
public static BatchItemType getItemType(final Map<String, Collection<String>> headers) {
final BatchItemType nextItemType;
final String contentType = headers.containsKey(HeaderName.contentType.toString())
? headers.get(HeaderName.contentType.toString()).toString() : StringUtils.EMPTY;
if (contentType.contains(ContentType.MULTIPART_MIXED.toContentTypeString())) {
nextItemType = BatchItemType.CHANGESET;
} else if (contentType.contains(ODataBatchConstants.ITEM_CONTENT_TYPE)) {
nextItemType = BatchItemType.RETRIEVE;
} else {
nextItemType = BatchItemType.NONE;
}
LOG.debug("Retrieved next item type {}", nextItemType);
return nextItemType;
}
/**
* Checks if the given line is the expected end-line.
*
* @param controller batch controller.
* @param line line to be checked.
* @return 'TRUE' if the line is not the end-line; 'FALSE' otherwise.
*/
private static boolean isNotEndLine(final ODataBatchController controller, final String line) {
return line == null
|| (StringUtils.isBlank(controller.getBoundary()) && StringUtils.isNotBlank(line))
|| (StringUtils.isNotBlank(controller.getBoundary()) && !line.startsWith(controller.getBoundary()));
}
}