/* ==================================================================
* BulkJsonWebPostUploadService.java - Aug 25, 2014 10:40:24 AM
*
* Copyright 2007-2014 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.upload.bulkjsonwebpost;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import net.solarnetwork.node.BulkUploadResult;
import net.solarnetwork.node.BulkUploadService;
import net.solarnetwork.node.domain.Datum;
import net.solarnetwork.node.reactor.Instruction;
import net.solarnetwork.node.reactor.InstructionAcknowledgementService;
import net.solarnetwork.node.reactor.InstructionStatus;
import net.solarnetwork.node.reactor.ReactorService;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicToggleSettingSpecifier;
import net.solarnetwork.node.support.JsonHttpClientSupport;
import net.solarnetwork.util.OptionalServiceTracker;
import org.springframework.context.MessageSource;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link BulkUploadService} that uses an HTTP POST with body content formed as
* a JSON document containing all data to upload.
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>objectMapper</dt>
* <dd>The {@link ObjectMapper} to marshall objects to JSON with and parse the
* response with.</dd>
*
* <dt>uploadEmptyDataset</dt>
* <dd>If <em>true</em> then make a POST request to SolarIn even if there isn't
* any datum data to upload. This can be useful in situations where we want to
* be able to receive instructions in the HTTP response even if the node has not
* produced any data to upload. Defaults to <em>false</em>.</dd>
* </dl>
*
* @author matt
* @version 1.3
*/
public class BulkJsonWebPostUploadService extends JsonHttpClientSupport implements BulkUploadService,
InstructionAcknowledgementService, SettingSpecifierProvider {
private String url = "/bulkUpload.do";
private OptionalServiceTracker<ReactorService> reactorService;
private boolean uploadEmptyDataset = false;
private MessageSource messageSource;
@Override
public String getKey() {
return "BulkJsonWebPostUploadService:" + getIdentityService().getSolarNetHostName();
}
@Override
public List<BulkUploadResult> uploadBulkDatum(Collection<Datum> data) {
if ( (data == null || data.size() < 1) && uploadEmptyDataset == false ) {
return Collections.emptyList();
}
List<UploadResult> uploadResults;
try {
uploadResults = upload(data);
} catch ( JsonParseException e ) {
throw new RuntimeException(e);
} catch ( IOException e ) {
throw new RuntimeException(e);
}
List<BulkUploadResult> results = new ArrayList<BulkUploadResult>(uploadResults.size());
Iterator<Datum> dataIterator = data.iterator();
for ( UploadResult r : uploadResults ) {
if ( !dataIterator.hasNext() ) {
break;
}
Datum datum = dataIterator.next();
results.add(new BulkUploadResult(datum, r.getId()));
}
return results;
}
@Override
public void acknowledgeInstructions(Collection<Instruction> instructions) {
try {
upload(instructions);
} catch ( JsonParseException e ) {
throw new RuntimeException(e);
} catch ( IOException e ) {
throw new RuntimeException(e);
}
}
/**
* Upload a collection of data objects, and parse the response into
* {@link UploadResult} objects.
*
* <p>
* The response is expected to be structured like this:
* </p>
*
* <pre>
* {
* "success" : true,
* "message" : "some message",
* "data" : {
* "datum" : [
* { "id" : "abc" ... },
* ...
* ],
* "instructions" : [
*
* ]
* }
* </pre>
*
* @param data
* @return
* @throws IOException
* @throws JsonParseException
*/
private List<UploadResult> upload(Collection<?> data) throws IOException, JsonParseException {
InputStream response = handlePost(data);
List<UploadResult> result = new ArrayList<UploadResult>(data.size());
try {
JsonNode root = getObjectMapper().readTree(response);
if ( root.isObject() ) {
JsonNode child = root.get("success");
if ( child != null && child.asBoolean() ) {
child = root.get("data");
if ( child != null && child.isObject() ) {
JsonNode datumArray = child.get("datum");
if ( datumArray != null && datumArray.isArray() ) {
for ( JsonNode element : datumArray ) {
UploadResult r = new UploadResult();
if ( element.has("id") ) {
r.setId(element.get("id").asText());
}
result.add(r);
}
}
JsonNode instrArray = child.get("instructions");
ReactorService reactor = (reactorService == null ? null : reactorService
.service());
if ( instrArray != null && instrArray.isArray() && reactor != null ) {
List<InstructionStatus> status = reactor.processInstruction(
getIdentityService().getSolarInBaseUrl(), instrArray,
JSON_MIME_TYPE, null);
log.debug("Instructions processed: {}", status);
}
} else {
log.debug("Upload returned no data.");
}
} else {
log.warn("Upload not successful: {}", root.get("message") == null ? "(no message)"
: root.get("message").asText());
}
}
} finally {
if ( response != null ) {
response.close();
}
}
return result;
}
private InputStream handlePost(Collection<?> data) {
final String postUrl = getIdentityService().getSolarInBaseUrl() + url;
try {
return doJson(postUrl, HTTP_METHOD_POST, data);
} catch ( IOException e ) {
if ( log.isTraceEnabled() ) {
log.trace("IOException bulk posting data to " + postUrl, e);
} else if ( log.isDebugEnabled() ) {
log.debug("Unable to post data: " + e.getMessage());
}
throw new RuntimeException(e);
}
}
// Settings
@Override
public String getSettingUID() {
return getClass().getName();
}
@Override
public String getDisplayName() {
return "Bulk JSON Upload Service";
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
BulkJsonWebPostUploadService defaults = new BulkJsonWebPostUploadService();
List<SettingSpecifier> result = new ArrayList<SettingSpecifier>();
result.add(new BasicToggleSettingSpecifier("uploadEmptyDataset", defaults.isUploadEmptyDataset()));
return result;
}
// Accessors
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public OptionalServiceTracker<ReactorService> getReactorService() {
return reactorService;
}
public void setReactorService(OptionalServiceTracker<ReactorService> reactorService) {
this.reactorService = reactorService;
}
public boolean isUploadEmptyDataset() {
return uploadEmptyDataset;
}
/**
* Flag to make HTTP POST requests even if there isn't any datum data to
* upload. This can be useful in situations where we want to be able to
* receive instructions in the HTTP response even if the node has not
* produced any data to upload.
*
* @param uploadEmptyDataset
* The upload empty data flag to set.
* @since 1.2
*/
public void setUploadEmptyDataset(boolean uploadEmptyDataset) {
this.uploadEmptyDataset = uploadEmptyDataset;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}