/**
* Global Sensor Networks (GSN) Source Code
* Copyright (c) 2006-2016, Ecole Polytechnique Federale de Lausanne (EPFL)
*
* This file is part of GSN.
*
* GSN 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 3 of the License, or
* (at your option) any later version.
*
* GSN 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 GSN. If not, see <http://www.gnu.org/licenses/>.
*
* File: src/ch/epfl/gsn/wrappers/AbstractWrapper.java
*
* @author gsn_devs
* @author Ali Salehi
* @author Mehdi Riahi
* @author Timotee Maret
* @author Julien Eberle
*
*/
package ch.epfl.gsn.wrappers;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import javax.naming.OperationNotSupportedException;
import org.slf4j.LoggerFactory;
import ch.epfl.gsn.Main;
import ch.epfl.gsn.beans.AddressBean;
import ch.epfl.gsn.beans.DataField;
import ch.epfl.gsn.beans.StreamElement;
import ch.epfl.gsn.beans.StreamSource;
import ch.epfl.gsn.beans.windowing.LocalTimeBasedSlidingHandler;
import ch.epfl.gsn.beans.windowing.RemoteTimeBasedSlidingHandler;
import ch.epfl.gsn.beans.windowing.SlidingHandler;
import ch.epfl.gsn.beans.windowing.TupleBasedSlidingHandler;
import ch.epfl.gsn.beans.windowing.WindowType;
import ch.epfl.gsn.monitoring.Monitorable;
import ch.epfl.gsn.utils.GSNRuntimeException;
import ch.epfl.gsn.wrappers.AbstractWrapper;
import org.slf4j.Logger;
public abstract class AbstractWrapper extends Thread implements Monitorable {
private final static transient Logger logger = LoggerFactory.getLogger(AbstractWrapper.class);
protected final List<StreamSource> listeners = Collections
.synchronizedList(new ArrayList<StreamSource>());
private AddressBean activeAddressBean;
private boolean isActive = true;
private SlidingHandler tupleBasedSlidingHandler;
private SlidingHandler timeBasedSlidingHandler;
private HashMap<Class<? extends SlidingHandler>, SlidingHandler> slidingHandlers = new HashMap<Class<? extends SlidingHandler>, SlidingHandler>();
private boolean usingRemoteTimestamp = false;
private Hashtable<Object,Long> lastInOrderTimestamp = new Hashtable<Object, Long>();
public static final int GARBAGE_COLLECT_AFTER_SPECIFIED_NO_OF_ELEMENTS = 2;
private Long oooCount = 0L;
private Long elementCount = 0L;
/**
* Returns the view name created for this listener. Note that, GSN creates
* one view per listener.
*
* @throws SQLException
*/
public void addListener(StreamSource ss) throws SQLException {
if (WindowType.isTimeBased(ss.getWindowingType())) {
if (timeBasedSlidingHandler == null) {
timeBasedSlidingHandler = isUsingRemoteTimestamp() == false ? new LocalTimeBasedSlidingHandler(
this)
: new RemoteTimeBasedSlidingHandler(this);
addSlidingHandler(timeBasedSlidingHandler);
}
} else {
if (tupleBasedSlidingHandler == null)
tupleBasedSlidingHandler = new TupleBasedSlidingHandler(this);
addSlidingHandler(tupleBasedSlidingHandler);
}
for (SlidingHandler slidingHandler : slidingHandlers.values()) {
if (slidingHandler.isInterestedIn(ss))
slidingHandler.addStreamSource(ss);
}
listeners.add(ss);
logger.debug("Adding listeners: " + ss.toString());
}
public void addSlidingHandler(SlidingHandler slidingHandler) {
slidingHandlers.put(slidingHandler.getClass(), slidingHandler);
}
/**
* Removes the listener with it's associated view.
*
* @throws SQLException
*/
public void removeListener(StreamSource ss) throws SQLException {
listeners.remove(ss);
// getStorageManager( ).executeDropView( ss.getUIDStr() );
for (SlidingHandler slidingHandler : slidingHandlers.values()) {
if (slidingHandler.isInterestedIn(ss))
slidingHandler.removeStreamSource(ss);
}
if (listeners.size() == 0) {
releaseResources();
}
}
/**
* @return the listeners
*/
public List<StreamSource> getListeners() {
return listeners;
}
//protected StorageManager getStorageManager() {
// return StorageManager.getInstance();
//
//}
/**
* This method is called whenever the wrapper wants to send a data item back
* to the source where the data is coming from. For example, If the data is
* coming from a wireless sensor network (WSN), This method sends a data
* item to the sink node of the virtual sensor. So this method is the
* communication between the System and actual source of data. The data sent
* back to the WSN could be a command message or a configuration message.
*
* @param dataItem
* : The data which is going to be send to the source of the data
* for this wrapper.
* @return True if the send operation is successful.
* @throws OperationNotSupportedException
* If the wrapper doesn't support sending the data back to the
* source. Note that by default this method throws this
* exception unless the wrapper overrides it.
*/
public boolean sendToWrapper(String action, String[] paramNames,
Object[] paramValues) throws OperationNotSupportedException {
throw new OperationNotSupportedException(
"This wrapper doesn't support sending data back to the source.");
}
public final AddressBean getActiveAddressBean() {
if (this.activeAddressBean == null) {
throw new RuntimeException(
"There is no active address bean associated with the wrapper.");
}
return activeAddressBean;
}
/**
* Only sets if there is no other activeAddressBean configured.
*
* @param newVal
* the activeAddressBean to set
*/
public void setActiveAddressBean(AddressBean newVal) {
if (this.activeAddressBean != null) {
throw new RuntimeException(
"There is already an active address bean associated with the wrapper.");
}
this.activeAddressBean = newVal;
}
private long noOfCallsToPostSE = 0;
private final transient int aliasCode = Main.getWindowStorage().tableNameGenerator();
private final CharSequence aliasCodeS = Main.getWindowStorage().tableNameGeneratorInString(aliasCode);
public int getDBAlias() {
return aliasCode;
}
public CharSequence getDBAliasInStr() {
return aliasCodeS;
}
public abstract DataField[] getOutputFormat();
public boolean isActive() {
return isActive;
}
protected void postStreamElement(Serializable... values) {
StreamElement se = new StreamElement(getOutputFormat(), values, System
.currentTimeMillis());
postStreamElement(se);
}
protected void postStreamElement(long timestamp, Serializable[] values) {
StreamElement se = new StreamElement(getOutputFormat(), values,
timestamp);
postStreamElement(se);
}
/**
* This method gets the generated stream element and notifies the input
* streams if needed. The return value specifies if the newly provided
* stream element generated at least one input stream notification or not.
*
* @param streamElement
* @return If the method returns false, it means the insertion doesn't
* effected any input stream.
*/
protected Boolean postStreamElement(StreamElement streamElement) {
if (streamElement == null) {
logger.info("postStreamElement is called with null ! Wrapper "
+ getWrapperName() + " might have a problem !");
return false;
}
try {
if (!isActive() || listeners.size() == 0)
return false;
if (!insertIntoWrapperTable(streamElement))
return false;
boolean toReturn = false;
logger.debug("Size of the listeners to be evaluated - "+ listeners.size());
for (SlidingHandler slidingHandler : slidingHandlers.values()) {
toReturn = slidingHandler.dataAvailable(streamElement)
|| toReturn;
}
if (++noOfCallsToPostSE
% GARBAGE_COLLECT_AFTER_SPECIFIED_NO_OF_ELEMENTS == 0) {
removeUselessValues();
}
return toReturn;
} catch (Exception e) {
logger.error(e.getMessage(), e);
logger.error("Produced data item from the wrapper couldn't be propagated inside the system.");
return false;
}
}
/**
* Updates the table representing the data items produced by the stream
* element. Returns false if the update fails or doesn't change the state of
* the table.
*
* @param se
* Stream element to be inserted to the table if needed.
* @return true if the stream element is successfully inserted into the
* table.
* @throws SQLException
*/
public boolean insertIntoWrapperTable(StreamElement se) throws SQLException {
if (listeners.size() == 0)
return false;
Connection conn = null;
try {
if (isOutOfOrder(se)) {
oooCount = oooCount == Long.MAX_VALUE ? 0 : oooCount + 1;
logger.debug("Out of order data item detected, it is not propagated into the system : [" + se.toString() + "]");
return false;
}
conn = Main.getWindowStorage().getConnection();
Main.getWindowStorage().executeInsert(aliasCodeS, getOutputFormat(), se, conn);
if (getPartialOrdersKey() != null){
lastInOrderTimestamp.put(se.getData(getPartialOrdersKey()), se.getTimeStamp());
}else{
lastInOrderTimestamp.put(0,se.getTimeStamp());
}
elementCount = elementCount == Long.MAX_VALUE ? 0 : elementCount + 1;
return true;
} finally {
Main.getWindowStorage().close(conn);
}
}
public boolean isOutOfOrder(StreamElement se) throws SQLException {
if (listeners.size() == 0)
return false;
Connection conn = null;
Object key = 0;
if (getPartialOrdersKey() != null){
key = se.getData(getPartialOrdersKey());
}
try {
// Checks if the stream element is out of order
if (lastInOrderTimestamp.get(key) == null) {
conn = Main.getWindowStorage().getConnection();
StringBuilder query = new StringBuilder();
query.append("select max(timed) from ").append(aliasCodeS);
StringBuilder query2 = new StringBuilder();
query2.append("select count(*) from ").append(aliasCodeS);
if (getPartialOrdersKey() != null){
query.append(" where "+getPartialOrdersKey()+"="+key); // code injection !!!
query2.append(" where "+getPartialOrdersKey()+"="+key);
}
ResultSet rs = Main.getWindowStorage().executeQueryWithResultSet(query,conn);
ResultSet rs2 = Main.getWindowStorage().executeQueryWithResultSet(query2,conn);
int n=rs2.next()?rs2.getInt(1):0;
if (rs.next() && n>0) {
lastInOrderTimestamp.put(key,rs.getLong(1));
} else {
lastInOrderTimestamp.put(key,Long.MIN_VALUE); // Table is empty
}
}
if (isTimeStampUnique())
return (se.getTimeStamp() <= lastInOrderTimestamp.get(key));
else
return (se.getTimeStamp() < lastInOrderTimestamp.get(key));
} finally {
Main.getWindowStorage().close(conn);
}
}
/**
* This method is called whenever the wrapper wants to send a data item back
* to the source where the data is coming from. For example, If the data is
* coming from a wireless sensor network (WSN), This method sends a data
* item to the sink node of the virtual sensor. So this method is the
* communication between the System and actual source of data. The data sent
* back to the WSN could be a command message or a configuration message.
*
* @param dataItem
* : The data which is going to be send to the source of the data
* for this wrapper.
* @return True if the send operation is successful.
* @throws OperationNotSupportedException
* If the wrapper doesn't support sending the data back to the
* source. Note that by default this method throws this
* exception unless the wrapper overrides it.
*/
public boolean sendToWrapper(Object dataItem)
throws OperationNotSupportedException {
if (isActive == false)
throw new GSNRuntimeException(
"Sending to an inactive/disabled wrapper is not allowed !");
throw new OperationNotSupportedException(
"This wrapper doesn't support sending data back to the source.");
}
/**
* Removes all the listeners, drops the views representing them, drops the
* sensor table, stops the TableSizeEnforce thread.
*
*/
public StringBuilder getUselessWindow() {
StringBuilder condition = new StringBuilder("");
synchronized (slidingHandlers) {
for (SlidingHandler slidingHandler : slidingHandlers.values()) {
if (condition.length()>0){condition.append(" and ");}
condition.append(slidingHandler.getCuttingCondition());
}
}
logger.debug("Cutting condition : " + condition);
if (condition.length() == 0)
return null;
StringBuilder sb = new StringBuilder("delete from ").append(
getDBAliasInStr()).append(" where ");
sb.append(condition);
return sb;
}
public int removeUselessValues() throws SQLException {
StringBuilder query = getUselessWindow();
if (query == null)
return 0;
logger.debug(new StringBuilder().append(
"RESULTING QUERY FOR Table Size Enforce ").append(query)
.toString());
int deletedRows = Main.getWindowStorage().executeUpdate(query);
logger.debug(new StringBuilder().append(deletedRows).append(
" old rows dropped from ").append(getDBAliasInStr())
.toString());
return deletedRows;
}
public void releaseResources() throws SQLException {
isActive = false;
Main.getInstance().getToMonitor().remove(this);
dispose();
logger.info("dispose called");
listeners.clear();
for (SlidingHandler slidingHandler : slidingHandlers.values()) {
slidingHandler.dispose();
}
Main.getWindowStorage().executeDropTable(aliasCodeS);
}
public static final String TIME_FIELD = "timed";
public final boolean initialize_wrapper(){
boolean r = initialize();
if (r){
Main.getInstance().getToMonitor().add(this);
setName(getWrapperName()+"::"+activeAddressBean.getVirtualSensorName());
}
return r;
}
/**
* The addressing is provided in the ("ADDRESS",Collection<KeyValue>). If
* the DataSource can't initialize itself because of either internal error
* or inaccessibility of the host specified in the address the method
* returns false. The dbAliasName of the DataSource is also specified with
* the "DBALIAS" in the context. The "STORAGEMAN" points to the
* StorageManager which should be used for querying.
*
* @return True if the initialization do successfully otherwise false;
*/
public abstract boolean initialize();
public abstract void dispose();
public abstract String getWrapperName();
/**
* Indicates whether we use GSN's time (local time) or the time already
* exists in the data (remote time) for the timestamp of generated stream
* elements.
*
* @return <code>false</code> if we use local time <br>
* <code>true</code> if we use remote time
*/
protected boolean isUsingRemoteTimestamp() {
return usingRemoteTimestamp;
}
/**
*
* @param usingRemoteTimestamp
*/
protected void setUsingRemoteTimestamp(boolean usingRemoteTimestamp) {
this.usingRemoteTimestamp = usingRemoteTimestamp;
}
/**
* Returns false if the wrapper can produce multiple different data items
* [stream elements] with the same timestamp. If this is false, then all the
* stream elements with the same timestamp will be accepted. If this method
* returns true (default value), duplicates are discarded and only the first
* one is kept.
*/
public boolean isTimeStampUnique() {
return true;
}
/**
* Allows for having partial ordering by only checking the time stamp order of
* stream elements having the same key.
* null is total ordering should be applied
*/
public String getPartialOrdersKey(){
return activeAddressBean.getPartialOrderKey();
}
public boolean manualDataInsertion(StreamElement se) {
throw new RuntimeException(
"Manual data insertion is not supported by this wrapper");
}
public Hashtable<String, Object> getStatistics(){
Hashtable<String, Object> stat = new Hashtable<String, Object>();
stat.put("vs."+activeAddressBean.getVirtualSensorName().replaceAll("\\.", "_")+".input."+ activeAddressBean.getInputStreamName().replaceAll("\\.", "_") +".outOfOrder.counter", oooCount);
stat.put("vs."+activeAddressBean.getVirtualSensorName().replaceAll("\\.", "_")+".input."+ activeAddressBean.getInputStreamName().replaceAll("\\.", "_") +".produced.counter", elementCount);
return stat;
}
}