/* ==================================================================
* BulkDataCollector.java - Feb 23, 2011 3:37:50 PM
*
* Copyright 2007-2011 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.beans.PropertyEditor;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import net.solarnetwork.central.RepeatableTaskException;
import net.solarnetwork.central.dao.SolarNodeDao;
import net.solarnetwork.central.datum.domain.Datum;
import net.solarnetwork.central.datum.domain.HardwareControlDatum;
import net.solarnetwork.central.in.biz.DataCollectorBiz;
import net.solarnetwork.central.instructor.domain.Instruction;
import net.solarnetwork.central.instructor.domain.InstructionState;
import net.solarnetwork.central.security.AuthenticatedNode;
import net.solarnetwork.central.security.SecurityException;
import net.solarnetwork.domain.NodeControlPropertyType;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessException;
import org.springframework.beans.PropertyAccessorFactory;
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;
/**
* Controller for accepting bulk upload XML documents.
*
* <p>
* This controller expects an {@link AuthenticatedNode} to be available on each
* request for securely processing node data.
* </p>
*
* @author matt
* @version 1.1
* @deprecated see {@link BulkJsonDataCollector}
*/
@Deprecated
@Controller
@RequestMapping(value = { "/bulkCollector.do", "/u/bulkCollector.do" })
public class BulkDataCollector extends AbstractDataCollector {
/** The InstructionStatus element name. */
public static final String INSTRUCTION_STATUS_ELEMENT_NAME = "InstructionStatus";
/** The NodeControlInfo element name. */
public static final String NODE_CONTROL_INFO_ELEMENT_NAME = "NodeControlInfo";
private static final String NODE_ID_GLOBAL_PARAM = "nodeId";
/**
* Default constructor.
*/
public BulkDataCollector() {
super();
}
/**
* Constructor.
*
* @param dataCollectorBiz
* the {@link DataCollectorBiz} to use
* @param solarNodeDao
* the {@link SolarNodeDao} to use
*/
@Autowired
public BulkDataCollector(DataCollectorBiz dataCollectorBiz, SolarNodeDao solarNodeDao) {
setDataCollectorBiz(dataCollectorBiz);
setSolarNodeDao(solarNodeDao);
}
/**
* 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
*/
@RequestMapping(method = RequestMethod.POST)
public String postData(@RequestHeader(value = "Content-Encoding", required = false) String encoding,
InputStream in, Model model) throws IOException {
AuthenticatedNode authNode = getAuthenticatedNode(false);
InputStream input = in;
if ( encoding != null && encoding.toLowerCase().contains("gzip") ) {
input = new GZIPInputStream(in);
}
List<Datum> parsedDatum = new ArrayList<Datum>();
List<Object> results = new ArrayList<Object>();
Map<String, Object> globalAttributes = new HashMap<String, Object>();
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
xmlInputFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE);
xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
try {
XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(input);
// once we pass the root element, assume all elements are datum records
boolean rootFound = false;
while ( reader.hasNext() ) {
switch (reader.next()) {
case XMLStreamReader.START_ELEMENT:
if ( rootFound ) {
Object o = handleElement(reader, globalAttributes);
if ( o instanceof Datum ) {
parsedDatum.add((Datum) o);
} else if ( o instanceof Instruction ) {
results.add(o);
}
} else {
int count = reader.getAttributeCount();
for ( int i = 0; i < count; i++ ) {
globalAttributes.put(reader.getAttributeLocalName(i),
reader.getAttributeValue(i));
}
if ( authNode != null ) {
// set nodeId to authenticated node ID
Long nodeId = authNode.getNodeId();
if ( nodeId != null ) {
globalAttributes.put(NODE_ID_GLOBAL_PARAM, authNode.getNodeId());
}
}
rootFound = true;
}
}
}
} catch ( XMLStreamException e ) {
throw new RuntimeException(e);
}
try {
List<Datum> postedDatum = getDataCollectorBiz().postDatum(parsedDatum);
results.addAll(postedDatum);
model.addAttribute(MODEL_KEY_RESULT, results);
} catch ( RepeatableTaskException e ) {
if ( log.isDebugEnabled() ) {
Throwable root = e;
while ( root.getCause() != null ) {
root = root.getCause();
}
log.debug("RepeatableTaskException caused by: " + root.getMessage());
}
}
if ( authNode != null ) {
defaultHandleNodeInstructions(authNode.getNodeId(), model);
}
return getViewName();
}
@ExceptionHandler(SecurityException.class)
public void handleSecurityException(SecurityException e, HttpServletResponse res) {
if ( log.isWarnEnabled() ) {
log.warn("Security exception: " + e.getMessage());
}
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
/**
* Parse InstructionStatus XML.
*
* <p>
* The XML looks like this:
* </p>
*
* <pre>
* <InstructionStatus instructionId="123" status="Received"/>
* </pre>
*
* @param reader
* the stream reader
* @param attributes
* global attributes
* @return
*/
private Object handleInstructionStatusElement(XMLStreamReader reader, Map<String, Object> attributes) {
String instructionId = reader.getAttributeValue(null, "instructionId");
String status = reader.getAttributeValue(null, "status");
Instruction result = null;
if ( instructionId != null && status != null && getInstructorBiz().isAvailable() ) {
Long id = Long.valueOf(instructionId);
InstructionState state = InstructionState.valueOf(status);
getInstructorBiz().getService().updateInstructionState(id, state);
result = new Instruction();
result.setId(id);
result.setState(state);
return result;
}
return result;
}
private Object handleElement(XMLStreamReader reader, Map<String, Object> attributes)
throws XMLStreamException {
// we are on a START_ELEMENT event here, so create bean from datum
// class and all attributes into JavaBean style properties
String className = reader.getLocalName();
if ( className.equals(INSTRUCTION_STATUS_ELEMENT_NAME) ) {
// handle instruction status specially
return handleInstructionStatusElement(reader, attributes);
} else if ( className.equals(NODE_CONTROL_INFO_ELEMENT_NAME) ) {
return handleNodeControlInfoElement(reader, attributes);
}
className = "net.solarnetwork.central.datum.domain." + className;
Datum datum = null;
try {
@SuppressWarnings("unchecked")
Class<? extends Datum> datumClass = (Class<? extends Datum>) Class.forName(className, true,
Datum.class.getClassLoader());
datum = datumClass.newInstance();
} catch ( ClassNotFoundException e ) {
if ( log.isWarnEnabled() ) {
log.warn("Unable to load Datum class " + className + " specified in XML");
}
return null;
} catch ( InstantiationException e ) {
if ( log.isWarnEnabled() ) {
log.warn("Unable to instantiate Datum class " + className + " specified in XML: "
+ e.getMessage());
}
return null;
} catch ( IllegalAccessException e ) {
if ( log.isWarnEnabled() ) {
log.warn("Unable to access Datum class " + className + " specified in XML: "
+ e.getMessage());
}
return null;
}
BeanWrapper bean = PropertyAccessorFactory.forBeanPropertyAccess(datum);
bean.registerCustomEditor(DateTime.class, (PropertyEditor) DATE_TIME_EDITOR.clone());
bean.registerCustomEditor(LocalDate.class, (PropertyEditor) LOCAL_DATE_EDITOR.clone());
bean.registerCustomEditor(LocalTime.class, (PropertyEditor) LOCAL_TIME_EDITOR.clone());
// apply global attributes
MutablePropertyValues pvs = new MutablePropertyValues(attributes);
bean.setPropertyValues(pvs, true, true);
int propertyCount = reader.getAttributeCount();
for ( int i = 0; i < propertyCount; i++ ) {
String attrName = reader.getAttributeLocalName(i);
if ( attrName.equalsIgnoreCase("id") ) {
// skip ID attributes on incoming data!
continue;
}
String attrValue = reader.getAttributeValue(i);
try {
bean.setPropertyValue(attrName, attrValue);
} catch ( InvalidPropertyException e ) {
if ( log.isDebugEnabled() ) {
log.debug("Unable to set property [" + attrName + "] to [" + attrValue
+ "] on instance of " + reader.getLocalName());
}
} catch ( PropertyAccessException e ) {
if ( log.isDebugEnabled() ) {
log.debug("Unable to set property [" + attrName + "] to [" + attrValue
+ "] on instance of " + reader.getLocalName());
}
}
}
if ( log.isTraceEnabled() ) {
log.trace("Parsed datum " + datum + " from XML");
}
return datum;
}
private Object handleNodeControlInfoElement(XMLStreamReader reader, Map<String, Object> attributes)
throws XMLStreamException {
HardwareControlDatum datum = null;
String controlId = reader.getAttributeValue(null, "controlId");
String propertyName = reader.getAttributeValue(null, "propertyName");
String value = reader.getAttributeValue(null, "value");
String type = reader.getAttributeValue(null, "type");
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;
}
}