/*
* Copyright (C) 2014 KAIST
* @author Wondeuk Yoon <wdyoon@resl.kaist.ac.kr>
*
* Copyright (C) 2007 ETH Zurich
*
* This file is part of Fosstrak (www.fosstrak.org).
*
* Fosstrak is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License version 2.1, as published by the Free Software Foundation.
*
* Fosstrak 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Fosstrak; if not, write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*/
package org.fosstrak.ale.server.cc.impl;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Logger;
import org.fosstrak.ale.exception.CCSpecValidationException;
import org.fosstrak.ale.exception.DuplicateSubscriptionException;
import org.fosstrak.ale.exception.ImplementationException;
import org.fosstrak.ale.exception.InvalidURIException;
import org.fosstrak.ale.exception.NoSuchSubscriberException;
import org.fosstrak.ale.server.cc.CommandCycle;
import org.fosstrak.ale.server.cc.ReportsGenerator;
import org.fosstrak.ale.server.cc.ReportsGeneratorState;
import org.fosstrak.ale.server.cc.Subscriber;
import org.fosstrak.ale.server.cc.impl.CommandCycleImpl;
import org.fosstrak.ale.server.impl.ReportsGeneratorImpl.NotificationThread;
import org.fosstrak.ale.server.util.CCSpecValidator;
import org.fosstrak.ale.util.ECTimeUnit;
import org.fosstrak.ale.xsd.ale.epcglobal.CCBoundarySpec.StartTriggerList;
import org.fosstrak.ale.xsd.ale.epcglobal.CCBoundarySpec.StopTriggerList;
import org.fosstrak.ale.xsd.ale.epcglobal.CCCmdReport;
import org.fosstrak.ale.xsd.ale.epcglobal.CCCmdReport.TagReports;
import org.fosstrak.ale.xsd.ale.epcglobal.CCCmdSpec;
import org.fosstrak.ale.xsd.ale.epcglobal.CCCmdSpec.OpSpecs;
import org.fosstrak.ale.xsd.ale.epcglobal.CCOpSpec;
import org.fosstrak.ale.xsd.ale.epcglobal.CCReports;
import org.fosstrak.ale.xsd.ale.epcglobal.CCSpec;
import org.fosstrak.ale.xsd.ale.epcglobal.ECReports;
import org.fosstrak.ale.xsd.ale.epcglobal.CCSpec.CmdSpecs;
import org.fosstrak.ale.xsd.ale.epcglobal.CCTagReport;
import org.fosstrak.ale.xsd.ale.epcglobal.ECTime;
import com.rits.cloning.Cloner;
/**
* default implementation of the reports generator.
* @author swieland
* @author Wondeuk Yoon
*
*/
public class ReportsGeneratorImpl implements ReportsGenerator, Runnable {
/**
* a negative interval means that no such interval is set.
*/
private static final long INTERVAL_NOT_SET = -1L;
/**
* period for how long to wait for calls in between waiting times.
*/
private static final long WAKEUP_PERIOD = 50L;
/** logger */
private static final Logger LOG = Logger.getLogger(ReportsGenerator.class);
/** name of the report generator */
private final String name;
/** cc specification which defines how the report should be generated */
private final CCSpec ccspec;
/** map of subscribers of this report generator */
private final Map<String, Subscriber> subscribers = new ConcurrentHashMap<String, Subscriber>();
// boundary spec values
/** start trigger */
private final StartTriggerList startTriggerValue;
/** stop trigger */
private final StopTriggerList stopTriggerValue;
/** time between one and the following command cycle in milliseconds */
private final long repeatPeriodValue;
/**
* The stable set interval in milliseconds. If there are no new tags
* detected for this time, the reports generation should stop.
*/
private final long stableSetInterval;
/** thread to run the main loop */
private Thread thread;
/** state of this report generator */
private ReportsGeneratorState state = ReportsGeneratorState.UNREQUESTED;
/** indicates if this report generator is running or not */
private boolean reportsGeneratorRunning = false;
/** indicates if somebody is polling this input generator at the moment. */
private boolean polling = false;
private CCReports pollReport = null;
private CommandCycle commandCycle = null;
private int AccessSpecNumber = 0;
private Hashtable<Integer, CCOpSpec> OpSpecTable = new Hashtable<Integer, CCOpSpec>();
private int OpSpecCount;
/**
* Constructor validates the cc specification and sets some parameters.
*
* @param name of this reports generator
* @param spec which defines how the reports of this generator should be build
* @throws ECSpecValidationException if the cc specification is invalid
* @throws ImplementationException if an implementation exception occurs
public ReportsGeneratorImpl(String name, CCSpec spec) throws CCSpecValidationException, ImplementationException {
this(name, spec, ALEApplicationContext.getBean(ECSpecValidator.class), ALEApplicationContext.getBean(ECReportsHelper.class));
}*/
/**
* Constructor validates the cc specification and sets some parameters.
*
* @param name of this reports generator
* @param spec which defines how the reports of this generator should be build
* @param validator the CCSpec validator to use for the validation of the CCSpec.
* @throws ECSpecValidationException if the cc specification is invalid
* @throws ImplementationException if an implementation exception occurs
*/
public ReportsGeneratorImpl(String name, CCSpec spec, CCSpecValidator validator/*, ECReportsHelper reportsHelper*/) throws CCSpecValidationException, ImplementationException {
LOG.debug("Try to create new ReportGenerator '" + name + "'.");
AccessSpecNumber = org.fosstrak.ale.server.cc.impl.ALECCImpl.getAccessSpecCounter();
org.fosstrak.ale.server.cc.impl.ALECCImpl.setAccessSpecCounter(AccessSpecNumber+1);
// set name
this.name = name;
//this.reportsHelper = reportsHelper;
// set spec
try {
validator.validateSpec(spec);
} catch (CCSpecValidationException e) {
LOG.error(e.getClass().getSimpleName() + ": " + e.getMessage(), e);
throw e;
} catch (ImplementationException e) {
LOG.error(e.getClass().getSimpleName() + ": " + e.getMessage(), e);
throw e;
}
//TODO : Validator. wdyoon
this.ccspec = spec;
// init boundary spec values
startTriggerValue = getCCStartTriggerValue();
stopTriggerValue = getCCStopTriggerValue();
repeatPeriodValue = getRepeatPeriodValue();
stableSetInterval = getStableSetInterval();
//Parsing OPSpec and register opname in to OPSpecTable
OpSpecCount = 1000;
List<CCCmdSpec> cmdspecs = ccspec.getCmdSpecs().getCmdSpec();
for (CCCmdSpec cccmdspec : cmdspecs)
{
List<CCOpSpec> opspecs = cccmdspec.getOpSpecs().getOpSpec();
for (CCOpSpec ccopspec : opspecs)
{
OpSpecTable.put(OpSpecCount, ccopspec);
OpSpecCount++;
}
}
LOG.debug(String.format("[startCCTriggerValue: %s, stopCCTriggerValue: %s, repeatPeriodValue: %s, stableSetInterval: %s]",
startTriggerValue, stopTriggerValue, repeatPeriodValue, stableSetInterval));
LOG.debug("ReportGenerator '" + name + "' successfully created.");
}
@Override
public Hashtable<Integer, CCOpSpec> getOpSpecTable() {
return OpSpecTable;
}
/**
* This method returns the cc specification of this generator.
*
* @return cc specification
*/
@Override
public CCSpec getCCSpec() {
// TODO Auto-generated method stub
return ccspec;
}
/**
* This method sets the state of this report generator.
* If the state changes from UNREQUESTED to REQUESTED, the report generators
* main loop will be started.
* If the state changes from REQUESTED to UNREQUESTED, the report generators
* main loop will be stopped.
* <strong>please notice that this method is not available through the ReportsGeneratorInterface.</strong>
*
* @param state to set
*/
public void setState(ReportsGeneratorState state) {
ReportsGeneratorState oldState = this.state;
this.state = state;
LOG.debug("ReportGenerator '" + name + "' change state from '" + oldState + "' to '" + state + "'");
if (isStateRequested() && !isRunning()) {
start();
} else if (isStateUnRequested() && isRunning()) {
stop();
}
}
/**
* This method returns the state of this report generator.
* <strong>please notice that this method is not available through the ReportsGeneratorInterface.</strong>
*
* @return state the state of the generator.
*/
public ReportsGeneratorState getState() {
return state;
}
/**
* This method subscribes a notification uri of a subscriber to this
* report generator.
* @param notificationURI to subscribe
* @throws DuplicateSubscriptionException if the specified notification uri
* is already subscribed
* @throws InvalidURIException if the notification uri is invalid
*/
@Override
public void subscribe(String notificationURI) throws DuplicateSubscriptionException, InvalidURIException {
Subscriber uri = new Subscriber(notificationURI);
if (subscribers.containsKey(notificationURI)) {
throw new DuplicateSubscriptionException(String.format("the URI is already subscribed on this specification %s, %s", name, uri));
} else {
subscribers.put(notificationURI, uri);
LOG.debug("NotificationURI '" + notificationURI + "' subscribed to spec '" + name + "'.");
if (isStateUnRequested()) {
setState(ReportsGeneratorState.REQUESTED);
}
}
}
/**
* This method unsubscribes a notification uri of a subscriber from this
* report generator.
* @param notificationURI to unsubscribe
* @throws NoSuchSubscriberException if the specified notification uri is
* not yet subscribed
* @throws InvalidURIException if the notification uri is invalid
*/
@Override
public void unsubscribe(String notificationURI) throws NoSuchSubscriberException, InvalidURIException {
// validate the URI:
new Subscriber(notificationURI);
if (subscribers.containsKey(notificationURI)) {
subscribers.remove(notificationURI);
LOG.debug("NotificationURI '" + notificationURI + "' unsubscribed from spec '" + name + "'.");
if (subscribers.isEmpty() && !isPolling()) {
setState(ReportsGeneratorState.UNREQUESTED);
}
} else {
throw new NoSuchSubscriberException("there is no subscriber on the given notification URI: " + notificationURI);
}
}
/**
* This method return the notification uris of all the subscribers of this
* report generator.
* @return list of notification uris
*/
@Override
public List<String> getSubscribers() {
return new ArrayList<String>(subscribers.keySet());
}
/**
* This method notifies all subscribers of this report generator about the
* specified cc reports.
* @param reports to notify the subscribers about
*/
@Override
public void notifySubscribers(CCReports reports, CommandCycle cc) {
// according the ALE 1.1 standard:
// When the processing of reportIfEmpty and reportOnlyOnChange
// results in all CCReport instances being omitted from an
// CCReports for an command cycle, then the delivery of results
// to subscribers SHALL be suppressed altogether. [...] poll
// and immediate SHALL always be returned [...] even if that
// CCReports instance contains zero CCReport instances.
// An CCReports instance SHALL include an CCReport instance corresponding to each
// CCReportSpec in the governing CCSpec, in the same order specified in the CCSpec,
// except that an CCReport instance SHALL be omitted under the following circumstances:
// - If an CCReportSpec has reportIfEmpty set to false, then the corresponding
// CCReport instance SHALL be omitted from the CCReports for this evcommandycle if
// the final, filtered set of Tags is empty (i.e., if the final Tag list would be empty, or if
// the final count would be zero).
// - If an ECReportSpec has reportOnlyOnChange set to true, then the
// corresponding ECReport instance SHALL be omitted from the ECReports for
// this command cycle if the filtered set of Tags is identical to the filtered prior set of Tags,
// where equality is tested by considering the primaryKeyFields as specified in the
// ECSpec (see Section 8.2), and where the phrase 'the prior set of Tags' is as defined
// in Section 8.2.6. This comparison takes place before the filtered set has been modified
// based on reportSet or output parameters. The comparison also disregards
// whether the previous ECReports was actually sent due to the effect of this
// parameter, or the reportIfEmpty parameter.
// When the processing of reportIfEmpty and reportOnlyOnChange results in all
// ECReport instances being omitted from an ECReports for an command cycle, then the
// delivery of results to subscribers SHALL be suppressed altogether. That is, a result
// consisting of an ECReports having zero contained ECReport instances SHALL NOT
// be sent to a subscriber. (Because an ECSpec must contain at least one
// ECReportSpec, this can only arise as a result of reportIfEmpty or
// reportOnlyOnChange processing.) This rule only applies to subscribers (command cycle
// requestors that were registered by use of the subscribe method); an ECReports
// instance SHALL always be returned to the caller of immediate or poll at the end of
// an command cycle, even if that ECReports instance contains zero ECReport instances.
Cloner cloner = new Cloner();
// deep clone the original input in order to keep it as the
// next command cycles last cycle reports.
CCReports originalInput = cloner.deepClone(reports);
// we deep clone (clone not sufficient) for the pollers
// in order to deliver them the correct set of reports.
if (isPolling()) {
// deep clone for the pollers (poll and immediate)
pollReport = cloner.deepClone(reports);
}
// we remove the reports that are equal to the ones in the
// last command cycle. then we send the subscribers.
List<CCCmdReport> equalReps = new LinkedList<CCCmdReport> ();
List<CCCmdReport> reportsToNotify = new LinkedList<CCCmdReport> ();
try {
for (CCCmdReport r : reports.getCmdReports().getCmdReport()) {
final CCCmdSpec reportSpec = cc.getCmdReportSpecByName(r.getCmdSpecName());
boolean tagsInReport = hasTags(r);
// case no tags in report but report if empty
if (!tagsInReport && reportSpec.isReportIfEmpty()) {
LOG.debug("requesting empty for report: " + r.getCmdSpecName());
reportsToNotify.add(r);
} else if (tagsInReport) {
reportsToNotify.add(r);
}
// check for equal reports since last notification.
/*if (reportSpec.isReportOnlyOnChange()) {
// report from the previous CommandCycle run.
ECReport oldR = cc.getLastReports().get(r.getReportName());
// compare the new report with the old one.
if (reportsHelper.areReportsEqual(reportSpec, r, oldR)) {
equalReps.add(r);
}
}*/
//TODO: wdyoon
}
} catch (Exception e) {
LOG.error("caught exception while processing reports: ", e);
}
// check if the intersection of all reports to notify (including empty ones) and the equal ones is empty
// -> if so, do not notify at all.
//reportsToNotify.removeAll(equalReps);
// remove the equal reports
/*Reports re = reports.getCmdReports();
if (null != re) re.getReport().removeAll(equalReps);
LOG.debug("reports size: " + reports.getCmdReports().getCmdReport().size());
*/
// next step is to check, if the total report is empty (even if requestIfEmpty but when all reports are equal, do not deliver)
// notify the ECReports
notifySubscribersWithFilteredReports(reports);
// store the new reports as old reports
/*ec.getLastReports().clear();
if (null != originalInput.getCmdReports()) {
for (ECReport r : originalInput.getCmdReports().getCmdReport()) {
ec.getLastReports().put(r.getReportName(), r);
}
}*/
//TODO: wdyoon
// notify pollers
// pollers always receive reports (even when empty).
if (isPolling()) {
polling = false;
if (subscribers.isEmpty()) {
setState(ReportsGeneratorState.UNREQUESTED);
}
synchronized (this) {
this.notifyAll();
}
}
}
/**
* check if a given CCCmdReport contains at least one tag in its data structures.
* @param r the report to check.
* @return true if tags contained, false otherwise.
*/
private boolean hasTags(CCCmdReport r) {
int count = 0;
try {
for (CCTagReport g : r.getTagReports().getTagReport()) {
if (g.getId().isEmpty() == false)
count++;
}
if (count > 0)
return true;
} catch (Exception ex) {
LOG.debug("could not check for tag occurence - report considered not to containing tags", ex);
}
return false;
}
/**
* once all the filtering is done eventually notify the subscribers with the reports.
* @param reports the filtered reports.
*/
protected void notifySubscribersWithFilteredReports(CCReports reports) {
// notify subscribers
Thread threadNotify = new Thread(new NotificationThread(reports, subscribers));
threadNotify.start();
/*for (Subscriber listener : subscribers.values()) {
try {
listener.notify(reports);
} catch (Exception e) {
LOG.error("Could not notify subscriber '" + listener.toString(), e);
}
}*/
}
/**
* This method is invoked if somebody polls this report generator.
* The result of the polling can be picked up by the method getPollReports.
*/
@Override
public void poll() {
LOG.debug("Spec '" + name + "' polled.");
pollReport = null;
polling = true;
if (isStateUnRequested()) {
setState(ReportsGeneratorState.REQUESTED);
}
}
/**
* This method delivers the cc reports which have been generated because
* of a poll.
* @return cc reports
*/
@Override
public CCReports getPollCCReports() {
return pollReport;
}
/**
* This method starts the main loop of the report generator.
*/
protected void start() {
thread = new Thread(this, name);
thread.setDaemon(true);
setRunning(true);
thread.start();
LOG.debug("Thread of spec '" + name + "' started.");
}
/**
* This method stops the main loop of the report generator.
*/
public void stop() {
commandCycle.stop();
// stop Thread
setRunning(false);
thread.interrupt();
LOG.debug("Thread of spec '" + name + "' stopped.");
}
/**
* This method returns the name of this reports generator.
*
* @return name of reports generator
*/
@Override
public String getName() {
return name;
}
/**
* create a new CommandCycle that can be used by this reports generator.
* @return the newly created command cycle.
* @throws ImplementationException when the command cycle cannot be created.
*/
protected CommandCycle createCommandCycle() throws ImplementationException {
LOG.debug("creating new command cycle.");
return new CommandCycleImpl(this);
}
/**
* This method contains the main loop of the reports generator.
* Here the command cycles will be generated and started.
*/
@Override
public void run() {
try {
commandCycle = createCommandCycle();
} catch (ImplementationException e) {
LOG.error("could not create a new CommandCycle - aborting.", e);
return;
}
if (startTriggerValue != null) {
LOG.debug("start trigger defined - not invoking the command cycle start..");
if (!isRepeatPeriodSet()) {
// startTrigger is specified and repeatPeriod is not specified
// commandCycle is started when:
// state is REQUESTED and startTrigger is received
}
} else {
if (isRepeatPeriodSet()) {
// startTrigger is not specified and repeatPeriod is specified
// commandCycle is started when:
// state transitions from UNREQUESTED to REQUESTED or
// repeatPeriod has elapsed from start of the last commandCycle and
// in that interval the state was never UNREQUESTED
while (isRunning()) {
// wait until state is REQUESTED
synchronized (state) {
while (!isStateRequested()) {
try {
// wakeup the reports generator every once in a while
state.wait(WAKEUP_PERIOD);
} catch (InterruptedException e) {
LOG.debug("caught interrupted exception - leaving reports generator.");
return;
}
}
}
// while state is REQUESTED start every repeatPeriod a
// new CommandCycle
while (isStateRequested()) {
if (commandCycle == null) {
LOG.error("coommandCycle is null");
} else {
commandCycle.launch();
}
try {
synchronized (state) {
state.wait(repeatPeriodValue);
}
// wait for the command cycle to finish...
commandCycle.join();
} catch (InterruptedException e) {
LOG.debug("caught interrupted exception - leaving reports generator.");
return;
}
}
LOG.debug("Stopping ReportsGenerator " + getName());
}
} else {
// neither startTrigger nor repeatPeriod are specified
// commandCycle is started when:
// state transitions from UNREQUESTED to REQUESTED or
// immediately after the previous command cycle, if the state
// is still REQUESTED
while (isRunning()) {
// wait until state is REQUESTED
while (!isStateRequested()) {
try {
synchronized (state) {
state.wait();
}
} catch (InterruptedException e) {
LOG.debug("caught interrupted exception - leaving reports generator.");
return;
}
}
// while state is REQUESTED start one CommandCycle
// after the other
while (isStateRequested()) {
commandCycle.launch();
while (!commandCycle.isTerminated()) {
try {
synchronized (commandCycle) {
commandCycle.wait(WAKEUP_PERIOD);
}
} catch (InterruptedException e) {
LOG.debug("caught interrupted exception - leaving reports generator.");
return;
}
}
}
}
LOG.debug("Stopping ReportsGenerator " + getName());
}
}
}
@Override
public void setStateRequested() {
setState(ReportsGeneratorState.REQUESTED);
}
@Override
public void setStateUnRequested() {
setState(ReportsGeneratorState.UNREQUESTED);
}
@Override
public boolean isStateRequested() {
return state == ReportsGeneratorState.REQUESTED;
}
@Override
public boolean isStateUnRequested() {
return state == ReportsGeneratorState.UNREQUESTED;
}
/**
* This method returns the repeat period value on the basis of the command
* cycle specification.
* @return repeat period value or NO_REPEAT_PERIOD if none set.
* @throws ImplementationException if the time unit in use is unknown
*/
private long getRepeatPeriodValue() throws ImplementationException {
ECTime repeatPeriod = ccspec.getBoundarySpec().getRepeatPeriod();
if (repeatPeriod != null) {
if (repeatPeriod.getUnit().compareToIgnoreCase(ECTimeUnit.MS) != 0) {
throw new ImplementationException("The only ECTimeUnit allowed is milliseconds (MS).");
} else {
return repeatPeriod.getValue();
}
}
return INTERVAL_NOT_SET;
}
private boolean isRepeatPeriodSet() {
return repeatPeriodValue != INTERVAL_NOT_SET;
}
/**
* This method returns the start trigger value on the basis of the command
* cycle specification.
* @return start trigger value
*/
private StartTriggerList getCCStartTriggerValue() {
StartTriggerList startTrigger = ccspec.getBoundarySpec().getStartTriggerList();
if (startTrigger != null) {
return startTrigger;
}
return null;
}
/**
* This method returns the stop trigger value on the basis of the command
* cycle specification.
* @return stop trigger value
*/
private StopTriggerList getCCStopTriggerValue() {
StopTriggerList stopTrigger = ccspec.getBoundarySpec().getStopTriggerList();
if (stopTrigger != null) {
return stopTrigger;
}
return null;
}
/**
* This method returns the NoNewTagsInterval on the basis of the command
* cycle specification.
* @return stable set interval
*/
private long getStableSetInterval() {
ECTime stableSetInterval = ccspec.getBoundarySpec().getNoNewTagsInterval();
if (stableSetInterval != null) {
return stableSetInterval.getValue();
}
return INTERVAL_NOT_SET;
}
/**
* flags whether this reports generator is running or stopped.
* @return true if running, false otherwise.
*/
protected boolean isRunning() {
return reportsGeneratorRunning;
}
/**
* activate/deactivate a reports generator.
* @param runningState the new state of the reports generator.
*/
protected void setRunning(boolean runningState) {
reportsGeneratorRunning = runningState;
}
/**
* is the reports generator in polling state???
* @return true if polling, false otherwise.
*/
protected boolean isPolling() {
return polling;
}
/**
* the poll reports - <strong>Attention></strong> not null safe.
* @return the poll reports - <strong>Attention></strong> not null safe.
*/
protected CCReports getPollReport() {
return pollReport;
}
public int getAccessSpecNumber() {
return AccessSpecNumber;
}
public class NotificationThread implements Runnable {
private CCReports reports;
private Map<String, Subscriber> subscribers;
public NotificationThread(CCReports reports, Map<String, Subscriber> subscribers) {
this.reports = reports;
this.subscribers = subscribers;
}
@Override
public void run() {
// notify subscribers
for (Subscriber listener : subscribers.values()) {
try {
listener.notify(reports);
} catch (Exception e) {
LOG.error("Could not notify subscriber '" + listener.toString(), e);
}
}
}
}
}