/*
* Copyright (c) 2016 Red Hat, Inc. and/or its affiliates.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Cheng Fang - Initial API and implementation
*/
package org.jberet.support.io;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.batch.api.BatchProperty;
import javax.batch.api.chunk.ItemReader;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.client.WebTarget;
import org.jberet.support._private.SupportMessages;
/**
* An implementation of {@code ItemReader} that reads data items from REST resource.
* <p>
* Usage example:
* <pre>
* <chunk>
* <reader ref="restItemReader">
* <properties>
* <property name="restUrl" value="http://localhost:8080/appName/rest-api/movies"/>
*
* <!-- starts from item 3 for the initial reading, skipping the first 3 elements (0, 1, 2) -->
* <!-- if offset not set, will start reading from the beginning -->
* <property name="offset" value="3"/>
*
* <!-- configure each REST call to return a maximum 20 items -->
* <property name="limit" value="20"/>
*
* <!-- type of each element in REST response entity -->
* <property name="beanType" value="org.jberet.samples.wildfly.common.Movie"/>
* </properties>
* </reader>
* ...
* <chunk>
* </pre>
*
* @see RestItemWriter
* @see RestItemReaderWriterBase
*
* @since 1.3.0
*/
@Named
@Dependent
public class RestItemReader extends RestItemReaderWriterBase implements ItemReader {
/**
* Default key for offset query parameter.
*/
public static final String DEFAULT_OFFSET_KEY = "offset";
/**
* Default value for offset query parameter.
*/
public static final String DEFAULT_OFFSET = "0";
/**
* Default key for limit query parameter.
*/
public static final String DEFAULT_LIMIT_KEY = "limit";
/**
* Default value for limit query parameter.
*/
public static final String DEFAULT_LIMIT = "10";
/**
* Configures the key of the query parameter that specifies the starting
* position to read in the target REST resource. For example, some REST
* resource may require {@code start} instead of {@code offset} query
* parameter for the same purpose.
* <p>
* This batch property is optional. If not set, the default key
* {@value #DEFAULT_OFFSET_KEY} is used.
*/
@Inject
@BatchProperty
protected String offsetKey;
/**
* The value of the {@code offset} property, which specifies the starting
* point for reading. If not specified, it defaults to {@value #DEFAULT_OFFSET}.
*/
@Inject
@BatchProperty
protected String offset;
/**
* Configures the key of the query parameter that specifies the maximum
* number of items to return in the REST response. For example, some REST
* resource may require {@code count} instead of {@code limit} query
* parameter for the same purpose.
* <p>
* This batch property is optional. If not set, the default key
* {@value #DEFAULT_LIMIT_KEY} is used.
*/
@Inject
@BatchProperty
protected String limitKey;
/**
* The value of the {@code limit} property, which specifies the maximum
* number of items to read. If not specified, it defaults to {@value #DEFAULT_LIMIT}.
*/
@Inject
@BatchProperty
protected String limit;
/**
* The class of individual element of the response message entity. For example,
* <ul>
* <li>{@code java.lang.String}
* <li>{@code org.jberet.samples.wildfly.common.Movie}
* </ul>
*/
@Inject
@BatchProperty
protected Class beanType;
/**
* The class of the REST response message entity, and is a array of collection
* type whose component type is {@link #beanType}. For example,
* <ul>
* <li>{@code Movie[]}
* <li>{@code java.util.List<Movie>}
* <li>{@code java.util.Collection<Movie>}
* </ul>
*/
protected Class entityType;
/**
* Current reading position in the target resource, and is returned as
* the current checkpoint in {@link #checkpointInfo()} method.
*/
protected int readerPosition;
/**
* Internal buffer to hold multiple items retrieved as the {@link #readItem()}
* method only returns 1 item at a time.
*/
protected List<Object> recordsBuffer = new ArrayList<Object>();
/**
* During the reader opening, the REST client is instantiated, and
* {@code checkpoint}, if any, is used to position the reader properly.
*
* @param checkpoint checkpoint info, null for the first invocation in a new job execution
* @throws Exception if error occurs
*/
@SuppressWarnings("unchecked")
@Override
public void open(final Serializable checkpoint) throws Exception {
super.open(checkpoint);
if(httpMethod == null) {
httpMethod = HttpMethod.GET;
} else {
httpMethod = httpMethod.toUpperCase(Locale.ENGLISH);
if (!HttpMethod.GET.equals(httpMethod) && !HttpMethod.DELETE.equals(httpMethod)) {
throw SupportMessages.MESSAGES.invalidReaderWriterProperty(null, httpMethod, "httpMethod");
}
}
if (offsetKey == null) {
offsetKey = DEFAULT_OFFSET_KEY;
}
if (offset == null) {
offset = DEFAULT_OFFSET;
}
if (checkpoint != null) {
readerPosition = (Integer) checkpoint;
} else {
readerPosition = Integer.parseInt(offset) - 1;
}
if (limitKey == null) {
limitKey = DEFAULT_LIMIT_KEY;
}
if (limit == null) {
limit = DEFAULT_LIMIT;
}
if (beanType == null) {
entityType = Object[].class;
} else {
entityType = (java.lang.reflect.Array.newInstance(beanType, 0)).getClass();
}
}
/**
* Returns reader checkpoint info (int number), which is the last successfully read position.
*
* @return reader checkpoint info as int
*/
@Override
public Serializable checkpointInfo() {
return readerPosition;
}
/**
* Reads 1 record and return the result object, and updates the current read position.
* The REST operation retrieves a collection of records, which are cached in this
* reader class to mimic the read-one-item-at-a-time behavior.
* Therefore, the REST call is only made when the local cache does not contains any entries.
* If no more record can be retrieved via the REST call, null is returned.
*
* @return the REST response entity object
* @throws Exception if error occurs
*/
@SuppressWarnings("unchecked")
@Override
public Object readItem() throws Exception {
final int size = recordsBuffer.size();
readerPosition++;
if (size > 0) {
// take 1 item from the end of the buffer
// items were added to the buffer in the reverse order, so the end is the oldest item
return recordsBuffer.remove(size - 1);
}
final WebTarget target = client.target(restUrl)
.queryParam(offsetKey, readerPosition)
.queryParam(limitKey, limit);
final Object[] recordsArray = HttpMethod.GET.equals(httpMethod) ?
(Object[]) target.request().get(entityType) :
(Object[]) target.request().delete(entityType);
if (recordsArray.length == 0) {
return null;
}
// add (n-1) items to the buffer in reverse order, and directly return the first element
for (int i = recordsArray.length - 1; i > 0; i--) {
recordsBuffer.add(recordsArray[i]);
}
return recordsArray[0];
}
/**
* closes the REST client and sets it to null.
*/
@Override
public void close() {
super.close();
recordsBuffer.clear();
}
}