package edu.sc.seis.sod; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import edu.iris.Fissures.IfNetwork.Channel; import edu.iris.Fissures.IfNetwork.NetworkId; import edu.iris.Fissures.IfNetwork.NetworkNotFound; import edu.iris.Fissures.IfNetwork.Station; import edu.iris.Fissures.model.MicroSecondDate; import edu.iris.Fissures.model.TimeInterval; import edu.iris.Fissures.model.UnitImpl; import edu.iris.Fissures.network.ChannelIdUtil; import edu.iris.Fissures.network.ChannelImpl; import edu.iris.Fissures.network.NetworkAttrImpl; import edu.iris.Fissures.network.NetworkIdUtil; import edu.iris.Fissures.network.StationIdUtil; import edu.iris.Fissures.network.StationImpl; import edu.sc.seis.fissuresUtil.chooser.ClockUtil; import edu.sc.seis.fissuresUtil.database.NotFound; import edu.sc.seis.fissuresUtil.exceptionHandler.GlobalExceptionHandler; import edu.sc.seis.fissuresUtil.hibernate.ChannelGroup; import edu.sc.seis.fissuresUtil.hibernate.NetworkDB; import edu.sc.seis.fissuresUtil.time.MicroSecondTimeRange; import edu.sc.seis.sod.hibernate.SodDB; import edu.sc.seis.sod.source.SodSourceException; import edu.sc.seis.sod.source.event.EventSource; import edu.sc.seis.sod.source.network.FdsnStation; import edu.sc.seis.sod.source.network.InstrumentationFromDB; import edu.sc.seis.sod.source.network.LoadedNetworkSource; import edu.sc.seis.sod.source.network.NetworkQueryConstraints; import edu.sc.seis.sod.source.network.NetworkSource; import edu.sc.seis.sod.source.network.RetryNetworkSource; import edu.sc.seis.sod.status.Fail; import edu.sc.seis.sod.status.StringTree; import edu.sc.seis.sod.status.networkArm.NetworkMonitor; import edu.sc.seis.sod.subsetter.channel.ChannelEffectiveTimeOverlap; import edu.sc.seis.sod.subsetter.channel.ChannelSubsetter; import edu.sc.seis.sod.subsetter.channel.PassChannel; import edu.sc.seis.sod.subsetter.network.NetworkEffectiveTimeOverlap; import edu.sc.seis.sod.subsetter.network.NetworkSubsetter; import edu.sc.seis.sod.subsetter.network.PassNetwork; import edu.sc.seis.sod.subsetter.station.PassStation; import edu.sc.seis.sod.subsetter.station.StationEffectiveTimeOverlap; import edu.sc.seis.sod.subsetter.station.StationSubsetter; public class NetworkArm implements Arm { public NetworkArm(Element config) throws ConfigurationException { channelGrouper = new ChannelGrouper(Start.getRunProps().getChannelGroupingRules()); processConfig(config); refresh = new RefreshNetworkArm(this); } public void run() { try { SodDB sodDb = SodDB.getSingleton(); // lastQueryTime should be null if first time lastQueryTime = sodDb.getQueryTime(getInternalNetworkSource().getName(), ""); // only do timer if positive interval and waveform arm exists, otherwise run in thread if (getRefreshInterval().value > 0 && Start.getWaveformRecipe() != null) { Timer timer = new Timer("Refresh NetworkArm", true); long period = (long)getInternalNetworkSource().getRefreshInterval().getValue(UnitImpl.MILLISECOND); long firstDelay = lastQueryTime==null ? 0 : lastQueryTime.delayUntilNextRefresh(getRefreshInterval()); logger.debug("Refresh timer startup: period: "+period+" firstDelay: "+firstDelay+" last query: "+(lastQueryTime==null ? "null" : lastQueryTime.getTime())); timer.schedule(refresh, firstDelay, period); if (period == 0) { try { Thread.sleep(10); } catch(InterruptedException e) { } // give refresh time to start up } initialStartupFinished = true; } else { // no refresh, so do net arm in this thread once initialStartupFinished = true; refresh.run(); } // make sure any open read only db connetion is closed (ie from lastQueryTime above) SodDB.rollback(); } catch(Throwable e) { armFinished = true; Start.armFailure(this, e); } } public boolean isActive() { return !armFinished; } public String getName() { return "NetworkArm"; } public NetworkAttrImpl getNetwork(NetworkId network_id) throws NetworkNotFound { List<NetworkAttrImpl> netDbs = getSuccessfulNetworks(); MicroSecondDate beginTime = new MicroSecondDate(network_id.begin_time); String netCode = network_id.network_code; for (NetworkAttrImpl attr : netDbs) { if(netCode.equals(attr.get_code()) && new MicroSecondTimeRange(attr.getEffectiveTime()).contains(beginTime)) { return attr; } } throw new NetworkNotFound("No network for id: " + NetworkIdUtil.toString(network_id)); } private void processConfig(Element config) throws ConfigurationException { NodeList children = config.getChildNodes(); for(int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if(node instanceof Element) { loadConfigElement(SodUtil.load((Element)node, PACKAGES)); } // end of if (node instanceof Element) } // end of for (int i=0; i<children.getSize(); i++) edu.iris.Fissures.TimeRange timeRange = configureEffectiveTimeCheckers(); NetworkQueryConstraints constraints = new NetworkQueryConstraints(attrSubsetter, stationSubsetter, chanSubsetters, timeRange); getNetworkSource().setConstraints(constraints); } private edu.iris.Fissures.TimeRange configureEffectiveTimeCheckers() { EventArm arm = Start.getEventArm(); if(arm != null && !Start.getRunProps().allowDeadNets()) { EventSource[] sources = arm.getSources(); MicroSecondTimeRange fullTime = sources[0].getEventTimeRange(); for(int i = 1; i < sources.length; i++) { fullTime = new MicroSecondTimeRange(fullTime, sources[i].getEventTimeRange()); } edu.iris.Fissures.TimeRange eventQueryTimes = fullTime.getFissuresTimeRange(); netEffectiveSubsetter = new NetworkEffectiveTimeOverlap(eventQueryTimes); staEffectiveSubsetter = new StationEffectiveTimeOverlap(eventQueryTimes); chanEffectiveSubsetter = new ChannelEffectiveTimeOverlap(eventQueryTimes); return eventQueryTimes; } else { logger.debug("No implicit effective time constraint"); return null; } } public static final String[] PACKAGES = {"networkArm", "channel", "site", "station", "network"}; private void loadConfigElement(Object sodElement) throws ConfigurationException { if(sodElement instanceof NetworkSource) { internalFinder = new RetryNetworkSource((NetworkSource)sodElement); finder = new InstrumentationFromDB(internalFinder); } else if(sodElement instanceof NetworkSubsetter) { attrSubsetter = (NetworkSubsetter)sodElement; } else if(sodElement instanceof StationSubsetter) { stationSubsetter = (StationSubsetter)sodElement; } else if(sodElement instanceof ChannelSubsetter) { chanSubsetters.add((ChannelSubsetter)sodElement); } else { throw new ConfigurationException("Unknown configuration object: " + sodElement.getClass()); } } public void add(NetworkMonitor monitor) { synchronized(statusMonitors) { statusMonitors.add(monitor); } } public TimeInterval getRefreshInterval() { return getInternalNetworkSource().getRefreshInterval(); } public List<ChannelSubsetter> getChannelSubsetters() { return chanSubsetters; } /** * returns an array of SuccessfulNetworks. if the refreshInterval is valid * it gets the networks from the database(may be embedded or external). if * not it gets the networks again from the network server specified in the * networkFinder. After obtaining the Networks if processes them using the * NetworkSubsetter and returns the successful networks as an array of * NetworkDbObjects. * */ public List<NetworkAttrImpl> getSuccessfulNetworks() { synchronized(refresh) { /** null means that we have not yet gotten nets from the server, so wait. */ List<NetworkAttrImpl> cacheNets = loadNetworksFromDB(); while (cacheNets == null && lastQueryTime == null && ! Start.isArmFailure()) { // still null, maybe first time through logger.info("Waiting on initial network load"); refresh.notifyAll(); try { refresh.wait(1000); } catch(InterruptedException e) { } cacheNets = loadNetworksFromDB(); } return cacheNets; } } List<NetworkAttrImpl> loadNetworksFromDB() { synchronized(netGetSync) { // don't get nets while being reloaded List<NetworkAttrImpl> fromDB = getNetworkDB().getAllNetworks(); for (NetworkAttrImpl net : fromDB) { // this is for the side effect of creating // networkInfoTemplate stuff // avoids a null ptr later change(net, Status.get(Stage.NETWORK_SUBSETTER, Standing.SUCCESS)); } return fromDB; } } List<NetworkAttrImpl> getSuccessfulNetworksFromServer() throws SodSourceException { synchronized(netGetSync) { statusChanged("Getting networks"); logger.info("Getting networks from server"); ArrayList<NetworkAttrImpl> successes = new ArrayList<NetworkAttrImpl>(); List<? extends NetworkAttrImpl> allNets = getInternalNetworkSource().getNetworks(); logger.info("Found " + allNets.size() + " networks"); int i=0; for (NetworkAttrImpl attr : allNets) { try { if(netEffectiveSubsetter.accept(attr).isSuccess()) { StringTree result; try { result = attrSubsetter.accept(attr); } catch(Throwable t) { logger.debug("Network subsetter exception: ", t); result = new Fail(attrSubsetter, "Exception", t); } if(result.isSuccess()) { NetworkDB ndb = getNetworkDB(); int dbid = ndb.put(attr); NetworkDB.commit(); logger.info("store network: " + NetworkIdUtil.toStringNoDates(attr)+" " + attr.getDbid() + " " + dbid); successes.add(attr); change(attr, Status.get(Stage.NETWORK_SUBSETTER, Standing.SUCCESS)); } else { change(attr, Status.get(Stage.NETWORK_SUBSETTER, Standing.REJECT)); failLogger.info(NetworkIdUtil.toString(attr .get_id()) + " was rejected. "+result); } } else { change(attr, Status.get(Stage.NETWORK_SUBSETTER, Standing.REJECT)); failLogger.info(NetworkIdUtil.toString(attr .get_id()) + " was rejected because it wasn't active during the time range of requested events"); } } catch(Throwable th) { GlobalExceptionHandler.handle("Got an exception while trying getSuccessfulNetworks for the " + i + "th networkAccess (" +(attr==null?"null":NetworkIdUtil.toStringNoDates(attr)), th); } i++; } if(lastQueryTime == null && allNets.size() == 0) { // hard to imagine a network arm that can't find a single good network is useful, so just fail // only fail if first time (lastQueryTime==null) so a server fail // during a run will not cause a crash logger.warn(NO_NETWORKS_MSG); Start.simpleArmFailure(this, NO_NETWORKS_MSG); } logger.info(successes.size() + " networks passed"); statusChanged("Waiting for a request"); synchronized(refresh) { refresh.notifyAll(); } return successes; } } void finish() { armFinished = true; lastQueryTime = new QueryTime(getInternalNetworkSource().getName(), "", ClockUtil.now().getTimestamp()); SodDB.getSingleton().putQueryTime(lastQueryTime); SodDB.commit(); logger.info("Network arm finished."); for (ArmListener listener : armListeners) { listener.finished(this); } if (Start.getEventArm() != null && Start.getEventArm().getWaveformArmSync() != null) { synchronized(Start.getEventArm().getWaveformArmSync()) { Start.getEventArm().getWaveformArmSync().notifyAll(); } } } public void add(ArmListener listener) { armListeners.add(listener); if(armFinished == true) { listener.finished(this); } } public StationImpl[] getSuccessfulStations(NetworkAttrImpl net) { String netCode = net.get_code(); logger.debug("getSuccessfulStations: "+net.get_code()); synchronized(refresh) { while(refresh.isNetworkBeingReloaded(net.getDbid())) { try { refresh.notifyAll(); refresh.wait(); } catch(InterruptedException e) {} } if(allStationFailureNets.contains(NetworkIdUtil.toStringNoDates(net))) { return new StationImpl[0]; } // try db List<StationImpl> sta = getNetworkDB().getStationForNet((NetworkAttrImpl)net); if(sta.size() != 0) { logger.debug("getSuccessfulStations " + netCode + " - from db " + sta.size()); return sta.toArray(new StationImpl[0]); } else { allStationFailureNets.add(NetworkIdUtil.toStringNoDates(net)); return new StationImpl[0]; } } } StationImpl[] getSuccessfulStationsFromServer(NetworkAttrImpl net) { synchronized(this) { NetworkAttrImpl netAttr; try { netAttr = NetworkDB.getSingleton().getNetwork(net.getDbid()); } catch(NotFound e1) { // must not be in db yet??? throw new RuntimeException("Network not in db yet: "+NetworkIdUtil.toString(net)); } statusChanged("Getting stations for " + net.getName()); ArrayList<Station> arrayList = new ArrayList<Station>(); try { List<? extends StationImpl> stations = getInternalNetworkSource().getStations(netAttr); /* // network consistency for TA take a really long time due to pairwise comparison. if( ! NetworkConsistencyCheck.isConsistent(stations)) { failLogger.warn("Inconsistent stations for network: "+NetworkIdUtil.toString(net)); } */ for (StationImpl stationImpl : stations) { logger.debug("Station in NetworkArm: " + StationIdUtil.toString(stationImpl)); } for (StationImpl currStation : stations) { /* //disable for speed reasons if (! NetworkConsistencyCheck.isConsistent(net, currStation)) { failLogger.warn("Not consistent: "+StationIdUtil.toString(currStation.getId())); }*/ // hibernate gets angry if the network isn't the same object // as the one already in the thread's session currStation.setNetworkAttr(netAttr); StringTree effResult = staEffectiveSubsetter.accept(currStation, getNetworkSource()); if(effResult.isSuccess()) { StringTree staResult; try { staResult = stationSubsetter.accept(currStation, getNetworkSource()); } catch(Throwable t) { logger.debug("Station subsetter exception: ", t); staResult = new Fail(stationSubsetter, "Exception", t); } if(staResult.isSuccess()) { int dbid = getNetworkDB().put(currStation); logger.info("Store " + currStation.get_code() + " as " + dbid + " in " + getNetworkDB()); arrayList.add(currStation); change(currStation, Status.get(Stage.NETWORK_SUBSETTER, Standing.SUCCESS)); } else { change(currStation, Status.get(Stage.NETWORK_SUBSETTER, Standing.REJECT)); failLogger.info(StationIdUtil.toString(currStation.get_id()) + " was rejected: " + staResult); } } else { change(currStation, Status.get(Stage.NETWORK_SUBSETTER, Standing.REJECT)); failLogger.info(StationIdUtil.toString(currStation.get_id()) + " was rejected because the station was not active during the time range of requested events: " + effResult); } } NetworkDB.commit(); } catch(Exception e) { GlobalExceptionHandler.handle("Problem in method getSuccessfulStations for net " + NetworkIdUtil.toString(net.get_id()), e); NetworkDB.rollback(); } StationImpl[] rtnValues = new StationImpl[arrayList.size()]; rtnValues = (StationImpl[])arrayList.toArray(rtnValues); statusChanged("Waiting for a request"); logger.debug("getSuccessfulStations " + NetworkIdUtil.toStringNoDates(net) + " - from server " + rtnValues.length); if(rtnValues.length == 0) { allStationFailureNets.add(NetworkIdUtil.toStringNoDates(net)); } else { allStationFailureNets.remove(NetworkIdUtil.toStringNoDates(net)); } return rtnValues; } } /** * Obtains the Channels corresponding to the station, processes them using * the ChannelSubsetter and returns an array of those that pass * */ public List<ChannelImpl> getSuccessfulChannels(StationImpl station) { synchronized(refresh) { while(refresh.isNetworkBeingReloaded(((NetworkAttrImpl)station.getNetworkAttr()).getDbid()) || refresh.isStationBeingReloaded(station.getDbid())) { try { refresh.notifyAll(); refresh.wait(); } catch(InterruptedException e) {} } if(allChannelFailureStations.contains(StationIdUtil.toStringNoDates(station))) { return new ArrayList<ChannelImpl>(0); } // no dice, try db List<ChannelImpl> sta = getNetworkDB().getChannelsForStation(station); if(sta.size() != 0) { logger.debug("successfulChannels " + station.get_code() + " - from db " + sta.size()); return sta; } else { allChannelFailureStations.add(StationIdUtil.toStringNoDates(station)); return new ArrayList<ChannelImpl>(0); } } } List<ChannelImpl> getSuccessfulChannelsFromServer(StationImpl station, LoadedNetworkSource loadedNetworkSource) { synchronized(this) { statusChanged("Getting channels for " + station); List<ChannelImpl> successes = new ArrayList<ChannelImpl>(); try { List<? extends ChannelImpl> chansAtStation = loadedNetworkSource.getChannels(station); Status inProg = Status.get(Stage.NETWORK_SUBSETTER, Standing.IN_PROG); boolean needCommit = false; StationImpl dbSta = NetworkDB.getSingleton().getStation(station.getDbid()); for (ChannelImpl chan : chansAtStation) { /* // consistency check takes time and only does a warning, skip if (! NetworkConsistencyCheck.isConsistent(station, chan)) { failLogger.warn("Not consistent: "+ChannelIdUtil.toString(chan.getId())); } */ // make the assumption that the station in the channel is the same as the station retrieved earlier chan.getSite().setStation(dbSta); change(chan, inProg); StringTree effectiveTimeResult = chanEffectiveSubsetter.accept(chan, loadedNetworkSource); NetworkDB.flush(); if(effectiveTimeResult.isSuccess()) { NetworkDB.flush(); boolean accepted = true; synchronized(chanSubsetters) { for (ChannelSubsetter cur : chanSubsetters) { StringTree result; try { result = cur.accept(chan, loadedNetworkSource); } catch(Throwable t) { logger.debug("Channel subsetter exception: ", t); result = new Fail(cur, "Exception", t); } if(!result.isSuccess()) { change(chan, Status.get(Stage.NETWORK_SUBSETTER, Standing.REJECT)); String resultToString = result.toString(); if (result.toString().trim().length() == 0) { resultToString = cur.getClass().getName(); } failLogger.info("Rejected " + ChannelIdUtil.toString(chan.get_id()) + ": " + resultToString); accepted = false; break; } } } if(accepted) { getNetworkDB().put(chan); logger.debug("Accept "+ChannelIdUtil.toString(chan.get_id())); needCommit = true; successes.add(chan); change(chan, Status.get(Stage.NETWORK_SUBSETTER, Standing.SUCCESS)); } } else { change(chan, Status.get(Stage.NETWORK_SUBSETTER, Standing.REJECT)); failLogger.info(ChannelIdUtil.toString(chan.get_id()) + " was rejected because the channel was not active during the time range of requested events: " + effectiveTimeResult); } } if(needCommit) { NetworkDB.commit(); } } catch(Throwable e) { GlobalExceptionHandler.handle("Problem in method getSuccessfulChannels for " + StationIdUtil.toString(station.get_id()), e); NetworkDB.rollback(); } statusChanged("Waiting for a request"); if(successes.size() == 0) { allChannelFailureStations.add(StationIdUtil.toStringNoDates(station)); } else { allChannelFailureStations.remove(StationIdUtil.toStringNoDates(station)); } return successes; } } public List<ChannelGroup> getSuccessfulChannelGroups(StationImpl station) { if(! refresh.isNetworkBeingReloaded(((NetworkAttrImpl)station.getNetworkAttr()).getDbid()) && allChannelGroupFailureStations.contains(StationIdUtil.toStringNoDates(station))) { return new ArrayList<ChannelGroup>(0); } synchronized(refresh) { while(refresh.isNetworkBeingReloaded(((NetworkAttrImpl)station.getNetworkAttr()).getDbid()) && refresh.isStationBeingReloaded(station.getDbid())) { try { refresh.notifyAll(); refresh.wait(); } catch(InterruptedException e) {} } if(allChannelGroupFailureStations.contains(StationIdUtil.toStringNoDates(station))) { return new ArrayList<ChannelGroup>(0); } // no dice, try db List<ChannelGroup> sta = getNetworkDB().getChannelGroupsForStation(station); if(sta.size() != 0) { logger.debug("successfulChannelGroups " + station.get_code() + " - from db " + sta.size()); return sta; } else { allChannelGroupFailureStations.add(StationIdUtil.toStringNoDates(station)); return new ArrayList<ChannelGroup>(0); } } } List<ChannelGroup> getSuccessfulChannelGroupsFromServer(StationImpl station, LoadedNetworkSource net) { synchronized(this) { List<ChannelImpl> failures = new ArrayList<ChannelImpl>(); List<ChannelGroup> chanGroups = channelGrouper.group(getSuccessfulChannelsFromServer(station, net), failures); for(ChannelGroup cg : chanGroups) { getNetworkDB().put(cg); } if (chanGroups.size() != 0) { NetworkDB.commit(); allChannelGroupFailureStations.remove(StationIdUtil.toStringNoDates(station)); } else { allChannelGroupFailureStations.add(StationIdUtil.toStringNoDates(station)); } for (ChannelImpl failchan : failures) { failLogger.info(ChannelIdUtil.toString(failchan.get_id()) + " Channel not grouped into 3 components."); } return chanGroups; } } private void statusChanged(String newStatus) { synchronized(statusMonitors) { for (NetworkMonitor netMon : statusMonitors) { try { netMon.setArmStatus(newStatus); } catch(Throwable e) { // caught for one, but should continue with rest after // logging // it GlobalExceptionHandler.handle("Problem changing status in NetworkArm", e); } } } } private void change(Channel chan, Status newStatus) { synchronized(statusMonitors) { for (NetworkMonitor netMon : statusMonitors) { try { netMon.change(chan, newStatus); } catch(Throwable e) { // caught for one, but should continue with rest after // logging // it GlobalExceptionHandler.handle("Problem changing channel status in NetworkArm", e); } } } } private void change(Station sta, Status newStatus) { synchronized(statusMonitors) { for (NetworkMonitor netMon : statusMonitors) { try { netMon.change(sta, newStatus); } catch(Throwable e) { // caught for one, but should continue with rest after // logging // it GlobalExceptionHandler.handle("Problem changing station status in NetworkArm", e); } } } } private void change(NetworkAttrImpl na, Status newStatus) { synchronized(statusMonitors) { for (NetworkMonitor netMon : statusMonitors) { try { netMon.change(na, newStatus); } catch(Throwable e) { // caught for one, but should continue with rest after // logging // it GlobalExceptionHandler.handle("Problem changing network status in NetworkArm", e); } } } } private NetworkSource internalFinder; private NetworkSource finder; private NetworkSubsetter attrSubsetter = new PassNetwork(); private NetworkSubsetter netEffectiveSubsetter = new PassNetwork(); private StationSubsetter stationSubsetter = new PassStation(); private StationSubsetter staEffectiveSubsetter = new PassStation(); private List<ChannelSubsetter> chanSubsetters = new ArrayList<ChannelSubsetter>(); private ChannelSubsetter chanEffectiveSubsetter = new PassChannel(); private ChannelGrouper channelGrouper; public NetworkSource getNetworkSource() { if (finder == null) { finder = new InstrumentationFromDB(getInternalNetworkSource()); } return finder; } protected NetworkSource getInternalNetworkSource() { if (internalFinder == null) { internalFinder = new RetryNetworkSource(new FdsnStation()); } return internalFinder; } protected NetworkDB getNetworkDB() { return NetworkDB.getSingleton(); } public boolean isBeingRefreshed(NetworkAttrImpl net) { return refresh.isNetworkBeingReloaded(net.getDbid()); } public boolean isBeingRefreshed(StationImpl sta) { return refresh.isStationBeingReloaded(sta.getDbid()); } public RefreshNetworkArm getRefresher() { return refresh; } private HashSet<String> allStationFailureNets = new HashSet<String>(); private HashSet<String> allChannelFailureStations = new HashSet<String>(); private HashSet<String> allChannelGroupFailureStations = new HashSet<String>(); private List<NetworkMonitor> statusMonitors = new ArrayList<NetworkMonitor>(); private List<ArmListener> armListeners = new ArrayList<ArmListener>(); private static Logger logger = LoggerFactory.getLogger(NetworkArm.class); private static final org.slf4j.Logger failLogger = org.slf4j.LoggerFactory.getLogger("Fail.NetworkArm"); private boolean armFinished = false; private boolean initialStartupFinished = false; public boolean isInitialStartupFinished() { return initialStartupFinished; } RefreshNetworkArm refresh; private QueryTime lastQueryTime = null; final Object netGetSync = new Object(); final Object staGetSync = new Object(); final Object chanGetSync = new Object(); public ChannelGrouper getChannelGrouper() { return channelGrouper; } public static final String NO_NETWORKS_MSG = "Found no networks. Make sure the network codes you entered are valid. " +"This can also be caused by asking for a restricted networks without <includeRestricted>true</includeRestricted> in a <fdsnStation> network source."; }// NetworkArm