/* * Copyright (C) 2014 KAIST * @author Janggwan Im <limg00n@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.impl; import java.util.ArrayList; import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Random; import java.util.Set; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import org.apache.log4j.Logger; import org.fosstrak.ale.exception.ECSpecValidationException; import org.fosstrak.ale.exception.ImplementationException; import org.fosstrak.ale.server.ALEApplicationContext; import org.fosstrak.ale.server.EventCycle; import org.fosstrak.ale.server.Report; import org.fosstrak.ale.server.ReportsGenerator; import org.fosstrak.ale.server.Tag; import org.fosstrak.ale.server.readers.LogicalReader; import org.fosstrak.ale.server.readers.LogicalReaderManager; import org.fosstrak.ale.util.ECTerminationCondition; import org.fosstrak.ale.util.ECTimeUnit; import org.fosstrak.ale.xsd.ale.epcglobal.ECReport; import org.fosstrak.ale.xsd.ale.epcglobal.ECReportSpec; import org.fosstrak.ale.xsd.ale.epcglobal.ECReports; import org.fosstrak.ale.xsd.ale.epcglobal.ECReports.Reports; import org.fosstrak.ale.xsd.ale.epcglobal.ECSpec; import org.fosstrak.ale.xsd.ale.epcglobal.ECTime; /** * default implementation of the event cycle. * * @author regli * @author swieland * @author benoit.plomion@orange.com * @author nkef@ait.edu.gr */ public final class EventCycleImpl implements EventCycle, Runnable { /** logger. */ private static final Logger LOG = Logger.getLogger(EventCycleImpl.class); /** random numbers generator. */ private static final Random rand = new Random(System.currentTimeMillis()); /** ale id. */ private static final String ALEID = "ETHZ-ALE" + rand.nextInt(); /** number of this event cycle. */ private static int number = 0; /** name of this event cycle. */ private final String name; /** report generator which contains this event cycle. */ private final ReportsGenerator generator; /** thread. */ private final Thread thread; /** event cycle specification for this event cycle. */ private final ECSpec spec; /** set of logical readers which deliver tags for this event cycle. */ private final Set<LogicalReader> logicalReaders = new HashSet<LogicalReader>(); /** set of reports for this event cycle. */ private final Set<Report> reports = new HashSet<Report>(); /** a hash map with all the reports generated in the last round. */ private final Map<String, ECReport> lastReports = new HashMap<String, ECReport> (); /** contains all the ec report specs hashed by their report name. */ private final Map<String, ECReportSpec> reportSpecByName = new HashMap<String, ECReportSpec> (); /** set of tags for this event cycle. */ private Set<Tag> tags = Collections.synchronizedSet(new HashSet<Tag>()); /** this set stores the tags from the previous EventCycle run. */ private Set<Tag> lastEventCycleTags = null; /** this set stores the tags between two event cycle in the case of rejectTagsBetweenCycle is false */ private Set<Tag> betweenEventsCycleTags = Collections.synchronizedSet(new HashSet<Tag>()); /** flags to know if the event cycle haven t to reject tags in the case than duration and repeatPeriod is same */ private boolean rejectTagsBetweenCycle = true; /** indicates if this event cycle is terminated or not .*/ private boolean isTerminated = false; /** * lock for thread synchronization between reports generator and this. * swieland 2012-09-29: do not use primitive type as int or Integer as autoboxing can result in new thread object for the lock -> non-threadsafe... */ private final EventCycleLock lock = new EventCycleLock(); /** flag whether the event cycle has passed through or not. */ private boolean roundOver = false; /** the duration of collecting tags for this event cycle in milliseconds. */ private long durationValue; /** the total time this event cycle runs in milliseconds. */ private long totalTime; /** the termination condition of this event cycle. */ private String terminationCondition = null; /** flags the eventCycle whether it shall run several times or not. */ private boolean running = false; /** flags whether the EventCycle is currently not accepting tags. */ private boolean acceptTags = false; /** tells how many times this EventCycle has been scheduled. */ private int rounds = 0; /** marks the start time of each event cycle */ long startTime = 0; /** tells whenDataAvailable is triggered in this round */ private boolean whenDataAvailableTriggered = false; // TODO: check if we can use this instead of the dummy class. private final class EventCycleLock { } /** * Constructor sets parameter and starts thread. * * @param generator to which this event cycle belongs to * @throws ImplementationException if an implementation exception occurs */ public EventCycleImpl(ReportsGenerator generator) throws ImplementationException { this(generator, ALEApplicationContext.getBean(LogicalReaderManager.class)); } /** * Constructor sets parameter and starts thread. * * @param generator to which this event cycle belongs to * @throws ImplementationException if an implementation exception occurs */ public EventCycleImpl(ReportsGenerator generator, LogicalReaderManager logicalReaderManager) throws ImplementationException { // set name name = generator.getName() + "_" + number++; // set ReportGenerator this.generator = generator; // set spec spec = generator.getSpec(); // get report specs and create a report for each spec for (ECReportSpec reportSpec : spec.getReportSpecs().getReportSpec()) { // add report spec and report to reports reports.add(new Report(reportSpec, this)); // hash into the report spec structure reportSpecByName.put(reportSpec.getReportName(), reportSpec); } // init BoundarySpec values durationValue = getDurationValue(); if(durationValue == 0) durationValue = getRepeatPeriodValue(); long repeatPeriod = getRepeatPeriodValue(); if (durationValue == repeatPeriod) { setRejectTagsBetweenCycle(false); } LOG.debug(String.format("durationValue: %s\n", durationValue)); setAcceptTags(false); LOG.debug("adding logicalReaders to EventCycle"); // get LogicalReaderStubs if (spec.getLogicalReaders() != null) { List<String> logicalReaderNames = spec.getLogicalReaders().getLogicalReader(); for (String logicalReaderName : logicalReaderNames) { LOG.debug("retrieving logicalReader " + logicalReaderName); LogicalReader logicalReader = logicalReaderManager.getLogicalReader(logicalReaderName); if (logicalReader != null) { LOG.debug("adding logicalReader " + logicalReader.getName() + " to EventCycle " + name); logicalReaders.add(logicalReader); } } } else { LOG.error("ECSpec contains no readers"); } for (LogicalReader logicalReader : logicalReaders) { // subscribe this event cycle to the logical readers LOG.debug("registering EventCycle " + name + " on reader " + logicalReader.getName()); logicalReader.addObserver(this); } rounds = 0; // create and start Thread thread = new Thread(this, "EventCycle" + name); thread.setDaemon(true); thread.start(); LOG.debug("New EventCycle '" + name + "' created."); } /** * This method returns the ec reports. * * @return ec reports * @throws ECSpecValidationException if the tags of the report are not valid * @throws ImplementationException if an implementation exception occurs. */ private ECReports getECReports() throws ECSpecValidationException, ImplementationException { // create ECReports ECReports reports = new ECReports(); // set spec name reports.setSpecName(generator.getName()); // set date try { reports.setDate(DatatypeFactory.newInstance().newXMLGregorianCalendar(new GregorianCalendar())); } catch (DatatypeConfigurationException e) { LOG.error("Could not create date: " + e.getMessage()); } // set ale id reports.setALEID(ALEID); // set total time in milliseconds reports.setTotalMilliseconds(totalTime); // set termination condition reports.setTerminationCondition(terminationCondition); // set spec if (spec.isIncludeSpecInReports()) { reports.setECSpec(spec); } // set reports reports.setReports(new Reports()); reports.getReports().getReport().addAll(getReportList()); return reports; } @Override public void addTag(Tag tag) { if (!isAcceptingTags()) { return; } // add event only if EventCycle is still running if (isEventCycleActive()) { logTagOnDebugEnabled(tag); // add tag to tags addTagAndLogOnNotAdded(tags, tag); } } /** * This method adds a tag between 2 event cycle. * * @param tag to add * @throws ImplementationException if an implementation exception occurs * @throws ECSpecValidationException if the tag is not valid */ private void addTagBetweenEventsCycle(Tag tag) { if (isRejectTagsBetweenCycle()) { return; } // add event only if EventCycle is still running if (isEventCycleActive()) { logTagOnDebugEnabled(tag); // add tag to tags addTagAndLogOnNotAdded(betweenEventsCycleTags, tag); } } /** * determine if this event cycle is active (running) or not. * @return true if the event cycle is active, false if not. */ private boolean isEventCycleActive() { return thread.isAlive(); } /** * log a tag to the logger if debug is enabled. the event cycles name and the tag as pure URI is written to the log on one line. * @param tagToLog the tag to be logged. */ private void logTagOnDebugEnabled(Tag tagToLog) { if (LOG.isDebugEnabled()) { LOG.debug("EventCycle '" + name + "' add Tag '" + tagToLog.getTagIDAsPureURI() + "'."); } } /** * little helper method adding a tag to a given set. if the tag is not added (as already contained) log it. * @param whereToAddTheTag the set where to add the tag to. * @param theTagToAdd the tag which is meant to be added. */ private void addTagAndLogOnNotAdded(Set<Tag> whereToAddTheTag, Tag theTagToAdd) { if (!whereToAddTheTag.add(theTagToAdd) && LOG.isDebugEnabled()) { LOG.debug("tag already contained, therefore not adding."); } } @Override public void update(Observable o, Object arg) { LOG.debug("EventCycle "+ getName() + ": Update notification received. "); List<Tag> tags = new LinkedList<Tag> (); // process the new tag. if (arg instanceof Tag) { LOG.debug("processing one tag"); // process one tag tags.add((Tag) arg); } else if (arg instanceof List) { LOG.debug("processing a list of tags"); for (Object entry : (List<?>) arg) { if (entry instanceof Tag) { tags.add((Tag) entry); } } } if (tags.size() > 0) { handleTags(tags); } else { LOG.debug("EventCycle "+ getName() + ": Update notification received - but not with any tags - ignoring. "); } } private void handleTags(List<Tag> tags) { if (!isAcceptingTags()) { handleTagsWhileNotAccepting(tags); } else { handleTagsWhileAccepting(tags); } } /** * deal with new tags. * @param tags */ private void handleTagsWhileAccepting(List<Tag> tags) { // process all the tags we did not process between two eventcycles (or while we did not accept any tags). if (!isRejectTagsBetweenCycle()) { for (Tag tag : betweenEventsCycleTags) { addTag(tag); } betweenEventsCycleTags.clear(); } LOG.debug("EventCycle "+ getName() + ": Received list of tags : set size is "+tags.size()); for (Tag tag : tags) { LOG.debug("EventCycle "+ getName() + ": Received list of tags :\t"+tag.getTagAsHex()); addTag(tag); } if(generator.isWhenDataAvailable()) { LOG.debug("WhenDataAvailable is triggered"); whenDataAvailableTriggered = true; synchronized (this) { this.notifyAll(); } } } /** * deal with tags while the event cycle is not accepting tags. (eg. between two event cycles). * @param arg the update we received. */ private void handleTagsWhileNotAccepting(List<Tag> tags) { if (!isRejectTagsBetweenCycle()) { for (Tag tag : tags) { LOG.debug("received tag between eventcycles: " + tag.getTagIDAsPureURI()); addTagBetweenEventsCycle(tag); } } } @Override public void stop() { // unsubscribe this event cycle from logical readers for (LogicalReader logicalReader : logicalReaders) { logicalReader.deleteObserver(this); } terminationCondition = ECTerminationCondition.UNDEFINE; running = false; thread.interrupt(); LOG.debug("EventCycle '" + name + "' stopped."); isTerminated = true; synchronized (this) { this.notifyAll(); } } @Override public String getName() { return name; } @Override public boolean isTerminated() { return isTerminated; } /** * This method is the main loop of the event cycle in which the tags will be collected. * At the end the reports will be generated and the subscribers will be notified. */ @Override public void run() { lastEventCycleTags = new HashSet<Tag>(); // wait for the start // running will be set by the ReportsGenerator when the EventCycle // has a subscriber if (!running) { synchronized (this) { try { this.wait(); } catch (InterruptedException e) { LOG.info("eventcycle got interrupted during not running state"); return; } } } while (running) { rounds ++; synchronized (lock) { roundOver = false; } LOG.info("EventCycle "+ getName() + ": Starting (Round " + rounds + ")."); // set start time startTime = System.currentTimeMillis(); // accept tags setAcceptTags(true); if(generator.isWhenDataAvailable()) { whenDataAvailableTriggered = false; } //------------------------------ run for the specified time try { if (durationValue > 0) { // if durationValue is specified and larger than zero, // wait for notify or durationValue elapsed. synchronized (this) { long dt = (System.currentTimeMillis() - startTime); terminationCondition = ECTerminationCondition.DURATION; this.wait(Math.max(1, durationValue - dt)); } } else { // if durationValue is not specified or smaller than zero, // wait for notify. synchronized (this) { this.wait(); } } } catch (InterruptedException e) { LOG.info("eventcycle got interrupted during running state"); // if Thread is stopped with method stop(), // then return without notify subscribers. // don't accept tags anymore setAcceptTags(false); //------------------------ generate the reports // get reports // compute total time totalTime = System.currentTimeMillis() - startTime; LOG.info("EventCycle "+ getName() + ": Number of Tags read in the current EventCyle.java: " + tags.size()); ECReports ecReports; try { ecReports = getECReports(); ecReports.setTerminationCondition("UNDEFINE"); // notifySubscribers generator.notifySubscribers(ecReports, this); } catch (ECSpecValidationException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } catch (ImplementationException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } return; } // don't accept tags anymore setAcceptTags(false); //------------------------ generate the reports // get reports try { // compute total time totalTime = System.currentTimeMillis() - startTime; LOG.info("EventCycle "+ getName() + ": Number of Tags read in the current EventCyle.java: " + tags.size()); ECReports ecReports = getECReports(); if(whenDataAvailableTriggered) { ecReports.setTerminationCondition("WhenDataAvailable"); } // notifySubscribers generator.notifySubscribers(ecReports, this); // store the current tags into the old tags // explicitly clear the tags if (lastEventCycleTags != null) { lastEventCycleTags.clear(); } if (null != tags) { lastEventCycleTags.addAll(tags); } tags = Collections.synchronizedSet(new HashSet<Tag>()); } catch (Exception e) { if (e instanceof InterruptedException) { LOG.info("eventcycle got interrupted during report generation"); return; } LOG.error("EventCycle "+ getName() + ": Could not create ECReports", e); } LOG.info("EventCycle "+ getName() + ": EventCycle finished (Round " + rounds + ")."); try { // inform possibly waiting workers about the finish synchronized (lock) { roundOver = true; lock.notifyAll(); } if(!whenDataAvailableTriggered) { // wait until reschedule. synchronized (this) { this.wait(); } } LOG.debug("eventcycle continues"); } catch (InterruptedException e) { LOG.info("eventcycle got interrupted during finished state"); return; } } // stop EventCycle stop(); } @Override public void launch() { this.running = true; LOG.debug("launching eventCycle" + getName()); synchronized (this) { this.notifyAll(); } } /** * This method returns all reports of this event cycle as event cycle * reports. * @return array of ec reports * @throws ECSpecValidationException if a tag of this report is not valid * @throws ImplementationException if an implementation exception occurs. */ private List<ECReport> getReportList() throws ECSpecValidationException, ImplementationException { ArrayList<ECReport> ecReports = new ArrayList<ECReport>(); for (Report report : reports) { ECReport r = report.getECReport(); if (null != r) ecReports.add(r); } return ecReports; } /** * This method returns the duration value extracted from the event cycle * specification. * @return duration value in milliseconds * @throws ImplementationException if an implementation exception occurs */ private long getDurationValue() throws ImplementationException { if (spec.getBoundarySpec() != null) { ECTime duration = spec.getBoundarySpec().getDuration(); if (duration != null) { if (duration.getUnit().compareToIgnoreCase(ECTimeUnit.MS) == 0) { return duration.getValue(); } else { throw new ImplementationException( "The only ECTimeUnit allowed is milliseconds (MS)."); } } } return -1; } /** * This method returns the repeat period value on the basis of the event * cycle specification. * @return repeat period value * @throws ImplementationException if the time unit in use is unknown */ private long getRepeatPeriodValue() throws ImplementationException { if (spec.getBoundarySpec() != null) { ECTime repeatPeriod = spec.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 -1; } @Override public Set<Tag> getLastEventCycleTags() { return copyContentToNewDatastructure(lastEventCycleTags); } @Override public Set<Tag> getTags() { return copyContentToNewDatastructure(tags); } /** * create a copy of the content of the given data structure -> we use synchronized sets -> make sure not to leak them.<br/> * this method synchronizes the original data structure during to copy process. * <br/> * <strong>Notice that the content is NOT cloned, simply referenced!</strong> * * @param contentToCopy the data structure to copy. * @return a copy of the data structure with the content of the input. */ private Set<Tag> copyContentToNewDatastructure(Set<Tag> contentToCopy) { Set<Tag> copy = new HashSet<Tag> (); synchronized (contentToCopy) { for (Tag tag : contentToCopy) { copy.add(tag); } } return copy; } private boolean isRejectTagsBetweenCycle() { return rejectTagsBetweenCycle; } private void setRejectTagsBetweenCycle(boolean rejectTagsBetweenCycle) { this.rejectTagsBetweenCycle = rejectTagsBetweenCycle; } /** * tells whether the ec accepts tags. * @return boolean telling whether the ec accepts tags */ private boolean isAcceptingTags() { return acceptTags; } /** * sets the flag acceptTags to the passed boolean value. * @param acceptTags sets the flag acceptTags to the passed boolean value. */ private void setAcceptTags(boolean acceptTags) { this.acceptTags = acceptTags; } @Override public int getRounds() { return rounds; } @Override public void join() throws InterruptedException { synchronized (lock) { while (!isRoundOver()) { lock.wait(); } } } /** * whether the event cycle round is over. * <strong>notice that this method is not exported via interface</strong>. * @return true if over, false otherwise */ public boolean isRoundOver() { return roundOver; } // FIXME: Implementation is currently leaking... need to do something. @Override public ECReportSpec getReportSpecByName(String name) { return reportSpecByName.get(name); } /** * get a handle onto the map holding all the report specs. * @return the map. */ protected Map<String, ECReportSpec> getReportSpecByName() { return reportSpecByName; } // FIXME: Implementation is currently leaking... need to do something. @Override public Map<String, ECReport> getLastReports() { return lastReports; } }