/* ================================================================== * BulkJsonDataCollector.java - Aug 25, 2014 10:53:53 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.central.in.web; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPInputStream; import javax.servlet.http.HttpServletResponse; import net.solarnetwork.central.RepeatableTaskException; import net.solarnetwork.central.dao.SolarNodeDao; import net.solarnetwork.central.datum.domain.Datum; import net.solarnetwork.central.datum.domain.GeneralLocationDatum; import net.solarnetwork.central.datum.domain.GeneralNodeDatum; import net.solarnetwork.central.datum.domain.HardwareControlDatum; import net.solarnetwork.central.in.biz.DataCollectorBiz; import net.solarnetwork.central.instructor.biz.InstructorBiz; import net.solarnetwork.central.instructor.domain.Instruction; import net.solarnetwork.central.instructor.domain.InstructionState; import net.solarnetwork.central.security.AuthenticatedNode; import net.solarnetwork.domain.NodeControlPropertyType; import net.solarnetwork.web.domain.Response; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** * JSON implementation of bulk upload service. * * @author matt * @version 1.3 */ @Controller @RequestMapping(value = { "/bulkCollector.do", "/u/bulkCollector.do" }, consumes = "application/json") public class BulkJsonDataCollector extends AbstractDataCollector { /** The JSON field name for an "object type". */ public static final String OBJECT_TYPE_FIELD = "__type__"; /** The InstructionStatus type. */ public static final String INSTRUCTION_STATUS_TYPE = "InstructionStatus"; /** The NodeControlInfo type. */ public static final String NODE_CONTROL_INFO_TYPE = "NodeControlInfo"; /** The {@link GeneralNodeDatum} or {@link GeneralLocationDatum} type. */ public static final String GENERAL_NODE_DATUM_TYPE = "datum"; /** * The JSON field name for a location ID on a {@link GeneralLocationDatum} * value. */ public static final String LOCATION_ID_FIELD = "locationId"; private final ObjectMapper objectMapper; /** * Constructor. * * @param dataCollectorBiz * the {@link DataCollectorBiz} to use * @param solarNodeDao * the {@link SolarNodeDao} to use * @param objectMapper * the {@link ObjectMapper} to use */ @Autowired public BulkJsonDataCollector(DataCollectorBiz dataCollectorBiz, SolarNodeDao solarNodeDao, ObjectMapper objectMapper) { setDataCollectorBiz(dataCollectorBiz); setSolarNodeDao(solarNodeDao); this.objectMapper = objectMapper; } /** * Handle a {@link RuntimeException}. * * @param e * the exception * @param response * the response * @return an error response object */ @ExceptionHandler(RuntimeException.class) @ResponseBody public Response<?> handleRuntimeException(RuntimeException e, HttpServletResponse response) { log.error("RuntimeException in {} controller", getClass().getSimpleName(), e); return new Response<Object>(Boolean.FALSE, null, "Internal error", null); } /** * Post new data. * * <p> * If {@code encoding} contains {@code gzip} the InputStream itself is * assumed to be compressed with GZip and encoded as Base64. Otherwise the * InputStream is assumed to be regular text (not compressed). * </p> * * @param encoding * an optional encoding value * @param in * the request input stream * @return the result model * @throws IOException * if any IO error occurs */ @ResponseBody @RequestMapping(method = RequestMethod.POST) public Response<BulkUploadResult> postData( @RequestHeader(value = "Content-Encoding", required = false) String encoding, InputStream in, Model model) throws IOException { AuthenticatedNode authNode = getAuthenticatedNode(true); InputStream input = in; if ( encoding != null && encoding.toLowerCase().contains("gzip") ) { input = new GZIPInputStream(in); } List<Datum> parsedDatum = new ArrayList<Datum>(); List<GeneralNodeDatum> parsedGeneralNodeDatum = new ArrayList<GeneralNodeDatum>(); List<GeneralLocationDatum> parsedGeneralLocationDatum = new ArrayList<GeneralLocationDatum>(); List<Object> resultDatum = new ArrayList<Object>(); try { JsonNode tree = objectMapper.readTree(input); if ( tree.isArray() ) { for ( JsonNode child : tree ) { Object o = handleNode(child); if ( o instanceof GeneralNodeDatum ) { parsedGeneralNodeDatum.add((GeneralNodeDatum) o); } else if ( o instanceof GeneralLocationDatum ) { parsedGeneralLocationDatum.add((GeneralLocationDatum) o); } else if ( o instanceof Datum ) { parsedDatum.add((Datum) o); } else if ( o instanceof Instruction ) { resultDatum.add(o); } } } } finally { if ( input != null ) { input.close(); } } try { if ( parsedDatum.size() > 0 ) { @SuppressWarnings("deprecation") List<Datum> postedDatum = getDataCollectorBiz().postDatum(parsedDatum); resultDatum.addAll(postedDatum); } if ( parsedGeneralNodeDatum.size() > 0 ) { getDataCollectorBiz().postGeneralNodeDatum(parsedGeneralNodeDatum); for ( GeneralNodeDatum d : parsedGeneralNodeDatum ) { resultDatum.add(d.getId()); } } if ( parsedGeneralLocationDatum.size() > 0 ) { getDataCollectorBiz().postGeneralLocationDatum(parsedGeneralLocationDatum); for ( GeneralLocationDatum d : parsedGeneralLocationDatum ) { resultDatum.add(d.getId()); } } } catch ( RepeatableTaskException e ) { if ( log.isDebugEnabled() ) { Throwable root = e; while ( root.getCause() != null ) { root = root.getCause(); } log.debug("RepeatableTaskException caused by: " + root.getMessage()); } } BulkUploadResult result = new BulkUploadResult(); if ( resultDatum.size() > 0 ) { result.setDatum(resultDatum); } // add instructions for the node InstructorBiz instructorBiz = (getInstructorBiz() == null ? null : getInstructorBiz().service()); if ( instructorBiz != null ) { List<Instruction> instructions = instructorBiz.getActiveInstructionsForNode(authNode .getNodeId()); if ( instructions != null && instructions.size() > 0 ) { result.setInstructions(instructions); } } return new Response<BulkUploadResult>(result); } private Object handleNode(JsonNode node) { String nodeType = getStringFieldValue(node, OBJECT_TYPE_FIELD, GENERAL_NODE_DATUM_TYPE); if ( GENERAL_NODE_DATUM_TYPE.equalsIgnoreCase(nodeType) ) { // if we have a location ID, this is actually a GeneralLocationDatum final JsonNode locId = node.get(LOCATION_ID_FIELD); if ( locId != null && locId.isNumber() ) { return handleGeneralLocationDatum(node); } return handleGeneralNodeDatum(node); } else if ( INSTRUCTION_STATUS_TYPE.equalsIgnoreCase(nodeType) ) { return handleInstructionStatus(node); } else if ( NODE_CONTROL_INFO_TYPE.equalsIgnoreCase(nodeType) ) { return handleNodeControlInfo(node); } else { return handleLegacyDatum(node); } } private String getStringFieldValue(JsonNode node, String fieldName, String placeholder) { JsonNode child = node.get(fieldName); return (child == null ? placeholder : child.asText()); } private HardwareControlDatum handleNodeControlInfo(JsonNode node) { HardwareControlDatum datum = null; String controlId = getStringFieldValue(node, "controlId", null); String propertyName = getStringFieldValue(node, "propertyName", null); String value = getStringFieldValue(node, "value", null); String type = getStringFieldValue(node, "type", null); if ( type != null && value != null ) { datum = new HardwareControlDatum(); NodeControlPropertyType t = NodeControlPropertyType.valueOf(type); switch (t) { case Boolean: if ( value.length() > 0 && (value.equals("1") || value.equalsIgnoreCase("yes") || value .equalsIgnoreCase("true")) ) { datum.setIntegerValue(Integer.valueOf(1)); } else { datum.setIntegerValue(Integer.valueOf(0)); } break; case Integer: datum.setIntegerValue(Integer.valueOf(value)); break; case Float: case Percent: datum.setFloatValue(Float.valueOf(value)); break; } String sourceId = controlId; if ( propertyName != null ) { sourceId += ";" + propertyName; } datum.setSourceId(sourceId); } return datum; } private Instruction handleInstructionStatus(JsonNode node) { String instructionId = getStringFieldValue(node, "instructionId", null); String status = getStringFieldValue(node, "status", null); Instruction result = null; InstructorBiz biz = getInstructorBiz().service(); if ( instructionId != null && status != null && biz != null ) { Long id = Long.valueOf(instructionId); InstructionState state = InstructionState.valueOf(status); biz.updateInstructionState(id, state); result = new Instruction(); result.setId(id); result.setState(state); return result; } return result; } private Object handleLegacyDatum(JsonNode node) { String className = getStringFieldValue(node, OBJECT_TYPE_FIELD, null); if ( className == null ) { return null; } className = "net.solarnetwork.central.datum.domain." + className; Class<?> datumClass = null; Object datum = null; try { datumClass = Class.forName(className, true, Datum.class.getClassLoader()); datum = objectMapper.treeToValue(node, datumClass); } catch ( ClassNotFoundException e ) { if ( log.isWarnEnabled() ) { log.warn("Unable to load Datum class " + className + " specified in JSON"); } return null; } catch ( IOException e ) { log.debug("Unable to parse JSON into {} class: {}", className, e.getMessage()); return null; } if ( log.isTraceEnabled() ) { log.trace("Parsed datum " + datum + " from JSON"); } return datum; } private GeneralNodeDatum handleGeneralNodeDatum(JsonNode node) { try { return objectMapper.treeToValue(node, GeneralNodeDatum.class); } catch ( IOException e ) { log.debug("Unable to parse JSON into GeneralNodeDatum: {}", e.getMessage()); return null; } } private GeneralLocationDatum handleGeneralLocationDatum(JsonNode node) { try { return objectMapper.treeToValue(node, GeneralLocationDatum.class); } catch ( IOException e ) { log.debug("Unable to parse JSON into GeneralLocationDatum: {}", e.getMessage()); return null; } } }