package com.sap.core.odata.core.batch; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Scanner; import java.util.regex.MatchResult; import java.util.regex.Pattern; import com.sap.core.odata.api.batch.BatchException; import com.sap.core.odata.api.client.batch.BatchSingleResponse; import com.sap.core.odata.api.commons.HttpContentType; import com.sap.core.odata.api.commons.HttpHeaders; import com.sap.core.odata.core.exception.ODataRuntimeException; public class BatchResponseParser { private static final String LF = "\n"; private static final String REG_EX_OPTIONAL_WHITESPACE = "\\s?"; private static final String REG_EX_ZERO_OR_MORE_WHITESPACES = "\\s*"; private static final String ANY_CHARACTERS = ".*"; private static final Pattern REG_EX_BLANK_LINE = Pattern.compile("(|" + REG_EX_ZERO_OR_MORE_WHITESPACES + ")"); private static final Pattern REG_EX_HEADER = Pattern.compile("([a-zA-Z\\-]+):" + REG_EX_OPTIONAL_WHITESPACE + "(.*)" + REG_EX_ZERO_OR_MORE_WHITESPACES); private static final Pattern REG_EX_VERSION = Pattern.compile("(?:HTTP/[0-9]\\.[0-9])"); private static final Pattern REG_EX_ANY_BOUNDARY_STRING = Pattern.compile("--" + ANY_CHARACTERS + REG_EX_ZERO_OR_MORE_WHITESPACES); private static final Pattern REG_EX_STATUS_LINE = Pattern.compile(REG_EX_VERSION + "\\s" + "([0-9]{3})\\s([\\S ]+)" + REG_EX_ZERO_OR_MORE_WHITESPACES); private static final Pattern REG_EX_BOUNDARY_PARAMETER = Pattern.compile(REG_EX_OPTIONAL_WHITESPACE + "boundary=(\".*\"|.*)" + REG_EX_ZERO_OR_MORE_WHITESPACES); private static final Pattern REG_EX_CONTENT_TYPE = Pattern.compile(REG_EX_OPTIONAL_WHITESPACE + HttpContentType.MULTIPART_MIXED); private static final String REG_EX_BOUNDARY = "([a-zA-Z0-9_\\-\\.'\\+]{1,70})|\"([a-zA-Z0-9_\\-\\.'\\+ \\(\\),/:=\\?]{1,69}[a-zA-Z0-9_\\-\\.'\\+\\(\\),/:=\\?])\""; // See RFC 2046 private String contentTypeMime; private String boundary; private String currentContentId; private int currentLineNumber = 0; public BatchResponseParser(final String contentType) { contentTypeMime = contentType; } public List<BatchSingleResponse> parse(final InputStream in) throws BatchException { Scanner scanner = new Scanner(in, BatchHelper.DEFAULT_ENCODING).useDelimiter(LF); List<BatchSingleResponse> responseList; try { responseList = Collections.unmodifiableList(parseBatchResponse(scanner)); } finally {// NOPMD (suppress DoNotThrowExceptionInFinally) scanner.close(); try { in.close(); } catch (IOException e) { throw new ODataRuntimeException(e); } } return responseList; } private List<BatchSingleResponse> parseBatchResponse(final Scanner scanner) throws BatchException { List<BatchSingleResponse> responses = new ArrayList<BatchSingleResponse>(); if (contentTypeMime != null) { boundary = getBoundary(contentTypeMime); parsePreamble(scanner); final String closeDelimiter = "--" + boundary + "--" + REG_EX_ZERO_OR_MORE_WHITESPACES; while (scanner.hasNext() && !scanner.hasNext(closeDelimiter)) { responses.addAll(parseMultipart(scanner, boundary, false)); } if (scanner.hasNext(closeDelimiter)) { scanner.next(closeDelimiter); currentLineNumber++; } else { throw new BatchException(BatchException.MISSING_CLOSE_DELIMITER.addContent(currentLineNumber)); } } else { throw new BatchException(BatchException.MISSING_CONTENT_TYPE); } return responses; } //The method parses additional information prior to the first boundary delimiter line private void parsePreamble(final Scanner scanner) { while (scanner.hasNext() && !scanner.hasNext(REG_EX_ANY_BOUNDARY_STRING)) { scanner.next(); currentLineNumber++; } } private List<BatchSingleResponse> parseMultipart(final Scanner scanner, final String boundary, final boolean isChangeSet) throws BatchException { Map<String, String> mimeHeaders = new HashMap<String, String>(); List<BatchSingleResponse> responses = new ArrayList<BatchSingleResponse>(); if (scanner.hasNext("--" + boundary + REG_EX_ZERO_OR_MORE_WHITESPACES)) { scanner.next(); currentLineNumber++; mimeHeaders = parseMimeHeaders(scanner); currentContentId = mimeHeaders.get(BatchHelper.HTTP_CONTENT_ID.toLowerCase(Locale.ENGLISH)); final String contentType = mimeHeaders.get(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ENGLISH)); if (contentType == null) { throw new BatchException(BatchException.MISSING_CONTENT_TYPE); } if (isChangeSet) { if (HttpContentType.APPLICATION_HTTP.equalsIgnoreCase(contentType)) { validateEncoding(mimeHeaders.get(BatchHelper.HTTP_CONTENT_TRANSFER_ENCODING.toLowerCase(Locale.ENGLISH))); parseNewLine(scanner);// mandatory BatchSingleResponseImpl response = parseResponse(scanner, isChangeSet); responses.add(response); } else { throw new BatchException(BatchException.INVALID_CONTENT_TYPE.addContent(HttpContentType.APPLICATION_HTTP)); } } else { if (HttpContentType.APPLICATION_HTTP.equalsIgnoreCase(contentType)) { validateEncoding(mimeHeaders.get(BatchHelper.HTTP_CONTENT_TRANSFER_ENCODING.toLowerCase(Locale.ENGLISH))); parseNewLine(scanner);// mandatory BatchSingleResponseImpl response = parseResponse(scanner, isChangeSet); responses.add(response); } else if (contentType.matches(REG_EX_OPTIONAL_WHITESPACE + HttpContentType.MULTIPART_MIXED + ANY_CHARACTERS)) { String changeSetBoundary = getBoundary(contentType); if (boundary.equals(changeSetBoundary)) { throw new BatchException(BatchException.INVALID_CHANGESET_BOUNDARY.addContent(currentLineNumber)); } parseNewLine(scanner);// mandatory Pattern changeSetCloseDelimiter = Pattern.compile("--" + changeSetBoundary + "--" + REG_EX_ZERO_OR_MORE_WHITESPACES); while (!scanner.hasNext(changeSetCloseDelimiter)) { responses.addAll(parseMultipart(scanner, changeSetBoundary, true)); } scanner.next(changeSetCloseDelimiter); currentLineNumber++; parseNewLine(scanner); } else { throw new BatchException(BatchException.INVALID_CONTENT_TYPE.addContent(HttpContentType.MULTIPART_MIXED + " or " + HttpContentType.APPLICATION_HTTP)); } } } else if (scanner.hasNext(boundary + REG_EX_ZERO_OR_MORE_WHITESPACES)) { currentLineNumber++; throw new BatchException(BatchException.INVALID_BOUNDARY_DELIMITER.addContent(currentLineNumber)); } else if (scanner.hasNext(REG_EX_ANY_BOUNDARY_STRING)) { currentLineNumber++; throw new BatchException(BatchException.NO_MATCH_WITH_BOUNDARY_STRING.addContent(boundary).addContent(currentLineNumber)); } else { currentLineNumber++; throw new BatchException(BatchException.MISSING_BOUNDARY_DELIMITER.addContent(currentLineNumber)); } return responses; } private BatchSingleResponseImpl parseResponse(final Scanner scanner, final boolean isChangeSet) throws BatchException { BatchSingleResponseImpl response = new BatchSingleResponseImpl(); if (scanner.hasNext(REG_EX_STATUS_LINE)) { scanner.next(REG_EX_STATUS_LINE); currentLineNumber++; final String statusCode; final String statusInfo; MatchResult result = scanner.match(); if (result.groupCount() == 2) { statusCode = result.group(1); statusInfo = result.group(2); } else { currentLineNumber++; throw new BatchException(BatchException.INVALID_STATUS_LINE.addContent(scanner.next()).addContent(currentLineNumber)); } Map<String, String> headers = parseResponseHeaders(scanner); parseNewLine(scanner); String contentLengthHeader = getHeaderValue(headers, HttpHeaders.CONTENT_LENGTH); String body = (contentLengthHeader != null) ? parseBody(scanner, Integer.parseInt(contentLengthHeader)) : parseBody(scanner); response.setStatusCode(statusCode); response.setStatusInfo(statusInfo); response.setHeaders(headers); response.setContentId(currentContentId); response.setBody(body); } else { currentLineNumber++; throw new BatchException(BatchException.INVALID_STATUS_LINE.addContent(scanner.next()).addContent(currentLineNumber)); } return response; } private void validateEncoding(final String encoding) throws BatchException { if (!BatchHelper.BINARY_ENCODING.equalsIgnoreCase(encoding)) { throw new BatchException(BatchException.INVALID_CONTENT_TRANSFER_ENCODING); } } private Map<String, String> parseMimeHeaders(final Scanner scanner) throws BatchException { Map<String, String> headers = new HashMap<String, String>(); while (scanner.hasNext() && !(scanner.hasNext(REG_EX_BLANK_LINE))) { if (scanner.hasNext(REG_EX_HEADER)) { scanner.next(REG_EX_HEADER); currentLineNumber++; MatchResult result = scanner.match(); if (result.groupCount() == 2) { String headerName = result.group(1).trim().toLowerCase(Locale.ENGLISH); String headerValue = result.group(2).trim(); headers.put(headerName, headerValue); } } else { throw new BatchException(BatchException.INVALID_HEADER.addContent(scanner.next())); } } return headers; } private Map<String, String> parseResponseHeaders(final Scanner scanner) throws BatchException { Map<String, String> headers = new HashMap<String, String>(); while (scanner.hasNext() && !scanner.hasNext(REG_EX_BLANK_LINE)) { if (scanner.hasNext(REG_EX_HEADER)) { scanner.next(REG_EX_HEADER); currentLineNumber++; MatchResult result = scanner.match(); if (result.groupCount() == 2) { String headerName = result.group(1).trim(); String headerValue = result.group(2).trim(); if (BatchHelper.HTTP_CONTENT_ID.equalsIgnoreCase(headerName)) { if (currentContentId == null) { currentContentId = headerValue; } } else { headers.put(headerName, headerValue); } } } else { currentLineNumber++; throw new BatchException(BatchException.INVALID_HEADER.addContent(scanner.next()).addContent(currentLineNumber)); } } return headers; } private String getHeaderValue(final Map<String, String> headers, final String headerName) { for (Map.Entry<String, String> header : headers.entrySet()) { if (headerName.equalsIgnoreCase(header.getKey())) { return header.getValue(); } } return null; } private String parseBody(final Scanner scanner) { StringBuilder body = null; while (scanner.hasNext() && !scanner.hasNext(REG_EX_ANY_BOUNDARY_STRING)) { if (!scanner.hasNext(REG_EX_ZERO_OR_MORE_WHITESPACES)) { if (body == null) { body = new StringBuilder(scanner.next()); } else { body.append(LF).append(scanner.next()); } } else { scanner.next(); } currentLineNumber++; } String responseBody = body != null ? body.toString() : null; return responseBody; } private String parseBody(final Scanner scanner, final int contentLength) { StringBuilder body = null; int length = 0; while (scanner.hasNext() && length < contentLength) { if (!scanner.hasNext(REG_EX_ZERO_OR_MORE_WHITESPACES)) { String nextLine = scanner.next(); length += BatchHelper.getBytes(nextLine).length; if (body == null) { body = new StringBuilder(nextLine); } else { body.append(LF).append(nextLine); } } else { scanner.next(); } currentLineNumber++; if (scanner.hasNext() && scanner.hasNext(REG_EX_BLANK_LINE)) { scanner.next(); currentLineNumber++; } } String responseBody = body != null ? body.toString() : null; return responseBody; } private String getBoundary(final String contentType) throws BatchException { Scanner contentTypeScanner = new Scanner(contentType).useDelimiter(";\\s?"); if (contentTypeScanner.hasNext(REG_EX_CONTENT_TYPE)) { contentTypeScanner.next(REG_EX_CONTENT_TYPE); } else { contentTypeScanner.close(); throw new BatchException(BatchException.INVALID_CONTENT_TYPE.addContent(HttpContentType.MULTIPART_MIXED)); } if (contentTypeScanner.hasNext(REG_EX_BOUNDARY_PARAMETER)) { contentTypeScanner.next(REG_EX_BOUNDARY_PARAMETER); MatchResult result = contentTypeScanner.match(); contentTypeScanner.close(); if (result.groupCount() == 1 && result.group(1).trim().matches(REG_EX_BOUNDARY)) { return trimQuota(result.group(1).trim()); } else { throw new BatchException(BatchException.INVALID_BOUNDARY); } } else { contentTypeScanner.close(); throw new BatchException(BatchException.MISSING_PARAMETER_IN_CONTENT_TYPE); } } private void parseNewLine(final Scanner scanner) throws BatchException { if (scanner.hasNext() && scanner.hasNext(REG_EX_BLANK_LINE)) { scanner.next(); currentLineNumber++; } else { currentLineNumber++; if (scanner.hasNext()) { throw new BatchException(BatchException.MISSING_BLANK_LINE.addContent(scanner.next()).addContent(currentLineNumber)); } else { throw new BatchException(BatchException.TRUNCATED_BODY.addContent(currentLineNumber)); } } } private String trimQuota(String boundary) { if (boundary.matches("\".*\"")) { boundary = boundary.replace("\"", ""); } boundary = boundary.replaceAll("\\)", "\\\\)"); boundary = boundary.replaceAll("\\(", "\\\\("); boundary = boundary.replaceAll("\\?", "\\\\?"); boundary = boundary.replaceAll("\\+", "\\\\+"); return boundary; } }