All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.coos.messaging.routing.DefaultRouter Maven / Gradle / Ivy

There is a newer version: 1.3.1
Show newest version
/**
 * COOS - Connected Objects Operating System (www.connectedobjects.org).
 *
 * Copyright (C) 2009 Telenor ASA and Tellu AS. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 *
 * You may also contact one of the following for additional information:
 * Telenor ASA, Snaroyveien 30, N-1331 Fornebu, Norway (www.telenor.no)
 * Tellu AS, Hagalokkveien 13, N-1383 Asker, Norway (www.tellu.no)
 */
package org.coos.messaging.routing;

import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.coos.messaging.COOS;
import org.coos.messaging.ConnectingException;
import org.coos.messaging.Link;
import org.coos.messaging.Message;
import org.coos.messaging.Processor;
import org.coos.messaging.ProcessorException;
import org.coos.messaging.ProcessorInterruptException;
import org.coos.messaging.impl.DefaultProcessor;
import org.coos.messaging.util.Log;
import org.coos.messaging.util.LogFactory;
import org.coos.messaging.util.URIHelper;
import org.coos.messaging.util.UuidGenerator;
import org.coos.messaging.util.UuidHelper;

/**
 * The DefaultRouter (actually default point to point router) contains the point
 * to point routing mechanisms
 * 
 * @author Knut Eilif Husa, Tellu AS
 */
public class DefaultRouter extends DefaultProcessor implements Router {

	private Collection preProcessors = new ConcurrentLinkedQueue();
	private Collection postProcessors = new ConcurrentLinkedQueue();

	private Map routingTables = new ConcurrentHashMap();
	private Map aliasTable = new ConcurrentHashMap();
	// todo let the links be handled by the coos instance
	private Map links = new ConcurrentHashMap();
	private Map routingAlgorithms = new ConcurrentHashMap();
	private Map segmentMapping = new ConcurrentHashMap();
	// The uuid of the router in the different segments it participates in. If
	// more than one uuid, this router is a gateway router
	private Collection routerUuids = new ConcurrentLinkedQueue();
	private Collection QoSClasses = new ConcurrentLinkedQueue();
	private String defaultQoSClass;
	private String COOSInstanceName;
	private boolean running = false;
	private boolean enabled = true;
	private boolean loggingEnabled = false;
	private Link defaultGw = null;

	private final Log logger = LogFactory.getLog(this.getClass(), false);
	private COOS COOS;

	// This constructor is only used by tests
	public DefaultRouter(String routerUuid) {
		COOSInstanceName = routerUuid;
		new LinkStateAlgorithm(this, routerUuid);
		addQoSClass(Link.DEFAULT_QOS_CLASS, true);
		addSegmentMapping(UuidHelper.getSegmentFromSegmentOrEndpointUuid(routerUuid), routerUuid, "linkstate");
	}

	public DefaultRouter() {
		addQoSClass(Link.DEFAULT_QOS_CLASS, true);
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public void removeRouterUuid(String routerUuid) {
		routerUuids.remove(routerUuid);
	}

	public void setLoggingEnabled(boolean loggingEnabled) {
		this.loggingEnabled = loggingEnabled;
		for (RoutingAlgorithm routingAlgorithm : routingAlgorithms.values()) {
			routingAlgorithm.setLoggingEnabled(loggingEnabled);
		}
	}

	public void processMessage(Message msg) {

		// Router preprocessors
		for (RouterProcessor routerProcessor : preProcessors) {
			try {
				routerProcessor.processMessage(msg);
			} catch (ProcessorInterruptException e) {
				return;
			} catch (ProcessorException e) {
				e.printStackTrace();
			}
		}

		if (msg.getReceiverEndpointUri() == null) {
			logger.warn("Message from " + msg.getSenderEndpointUri() + " missing receiver address");
			replyErrorReason(msg, Message.ERROR_NO_RECEIVER);
			return;
		}

		String qosClass = msg.getHeader(Message.QOS_CLASS);
		if (qosClass == null) {
			qosClass = defaultQoSClass;
		}

		Map routingTable = routingTables.get(qosClass);

		String hops = msg.getHeader(Message.HOPS);
		if (hops == null) {
			hops = "1";
		} else {
			hops = String.valueOf(Integer.parseInt(hops) + 1);
		}
		msg.setHeader(Message.HOPS, hops);
		if (Integer.parseInt(hops) > 244) {
			logger.warn("Message from " + msg.getSenderEndpointUri() + ", to: " + msg.getReceiverEndpointUri()
					+ " too many hops");
			replyErrorReason(msg, Message.ERROR_TOO_MANY_HOPS);
			return;
		}

		String uuid = resolveAlias(msg);

		if (uuid == null) {
			// will never occur except for localcoos
			replyErrorReason(msg, Message.ERROR_NO_ALIAS + ":" + msg.getReceiverEndpointUri());
			return;
		}

		Link link;

		// check if the message is destined towards this router
		if (routerUuids.contains(uuid)) {
			logger.putMDC(UUID_PREFIX, uuid);
			if (msg.getType().equals(Message.TYPE_ROUTING_INFO)) {
				routingAlgorithms.get(UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(uuid))
						.processRoutingInfo(msg);
			} else if (msg.getType().equals(Message.TYPE_ALIAS)) {
				try {
					setLinkAliases((Vector) msg.getBody(), msg.getMessageContext().getInBoundChannel().getOutLink());
				} catch (ProcessorException e) {
					e.printStackTrace();
				}
			}
		} else {

			// The routing step
			link = route(uuid, msg, routingTable);

			if (link != null) {
				msg.getMessageContext().setNextLink(link);
			}

			// Router postprocessors
			for (RouterProcessor routerProcessor : postProcessors) {
				try {
					routerProcessor.processMessage(msg);
				} catch (ProcessorInterruptException e) {
					return;
				} catch (ProcessorException e) {
					e.printStackTrace();
				}
			}

			// The sending step
			if (link != null) {

				if (msg.getHeader(Message.TRACE_ROUTE) != null) {
					String trace = msg.getHeader(Message.TRACE);
					if (trace == null) {
						trace = COOSInstanceName;
					}
					msg.setHeader(Message.TRACE, trace + " -> " + link.getDestinationUuid());
				}

				try {
					link.processMessage(msg);
				} catch (ProcessorException e) {
					replyErrorReason(msg, e.getMessage());
				}
			} else {
				String errorMsg;
				URIHelper helper = new URIHelper(msg.getReceiverEndpointUri());
				String alias = helper.getEndpoint();
				if (uuid.equals("null")) {
					errorMsg = COOSInstanceName + ": No uuid for alias " + alias + ", No route from "
							+ COOSInstanceName;
				} else {
					if (uuid.equals(alias)) {
						errorMsg = COOSInstanceName + ": No route to: " + alias + " from " + COOSInstanceName;
					} else {
						errorMsg = COOSInstanceName + ": No route to: " + alias + " / " + uuid + " from "
								+ COOSInstanceName;
					}
				}
				logger.warn(errorMsg);
				replyErrorReason(msg, Message.ERROR_NO_ROUTE);
			}
		}
	}

	/**
	 * This method does dynamic registers and unregisters aliases to link
	 * 
	 * @param regAliases
	 *            - An vector containing all aliases to be registered
	 * @param outlink
	 *            - The link to register the aliases with. This link is always
	 *            directed towards an endpoint
	 * @throws ProcessorException
	 */
	public void setLinkAliases(Vector regAliases, Link outlink) throws ProcessorException {
		String segment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(outlink.getDestinationUuid());
		if (!segment.equals(".")) {
			segment += ".";
		}
		// Qualify aliases
		for (int i = 0; i < regAliases.size(); i++) {
			String alias = (String) regAliases.get(i);
			if (!alias.startsWith(Router.DICO_SEGMENT + ".") 
					&& !alias.startsWith(Router.LOCAL_SEGMENT + ".") 
					&& !alias.startsWith(segment)) { 
				// unqualified aliases are qualified by prefixing with segment
				// name
				String qualifiedAlias = segment + alias;
				regAliases.remove(alias);
				regAliases.add(qualifiedAlias);
			}
		}

		Vector curAliases = outlink.getAlises();
		Iterator itCurAliases = curAliases.iterator();

		// remove aliases that are not present anymore
		while (itCurAliases.hasNext()) {
			String alias = (String) itCurAliases.next();
			if (!regAliases.contains(alias)) {
				itCurAliases.remove();
				removeAlias(alias);
			}
		}

		// Add all aliases
		Iterator itRegAliases = regAliases.iterator();
		while (itRegAliases.hasNext()) {
			String alias = (String) itRegAliases.next();
			outlink.addAlias(alias);
			String oldToUuid = aliasTable.get(alias);
			if (oldToUuid != null && !oldToUuid.equals(outlink.getDestinationUuid())) {
				Iterator itRegAliases2 = regAliases.iterator();
				while (itRegAliases2.hasNext()) {
					removeAlias((String) itRegAliases2.next());
				}

				throw new ProcessorException("Can not register alias:" + alias
						+ " since this alias is occupied for endpoint with uuid " + outlink.getDestinationUuid());
			}
			putAlias(alias, outlink.getDestinationUuid());
		}
	}

	/**
	 * This method resolves receiver URIs into uuids. It handles both URIs
	 * containing aliases and uuids
	 * 
	 * @param msg
	 *            the message to resolve alias for
	 * @return the uuid
	 */
	public String resolveAlias(Message msg) {
		URIHelper helper = new URIHelper(msg.getReceiverEndpointUri());
		String uuid;
		String alias;
		alias = helper.getEndpoint();
		String segment;

		if (alias != null && !helper.isEndpointUuid()) {

			// routing on alias
			
			if (!helper.isEndpointQualified()) {
				
				// To account for that not all messages arrives the router via an
				// incoming link.
				// I.e. the routingInfo from this router
				if (msg.getMessageContext().getInBoundLink() != null
						&& msg.getMessageContext().getInBoundLink().getDestinationUuid() != null) {
					segment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(msg.getMessageContext().getInBoundLink()
							.getDestinationUuid());
				} else {
					segment = ".";
				}

				if (!segment.equals(".")) {
					segment += ".";
				}
				
				// the alias is unqualified, first try to route in segment
				// namespace
				String qualifiedAlias = segment + alias;

				uuid = aliasTable.get(qualifiedAlias);
				if (uuid == null) {
					// If not found in segment namespace, then try to route in
					// dico namespace
					qualifiedAlias = DICO_SEGMENT + "." + alias;
					uuid = aliasTable.get(qualifiedAlias);
				}
			} else {
				// If fully qualified alias then lookup in aliasTable
				uuid = aliasTable.get(alias);
			}
			if (uuid == null  
					&& !helper.getSegment().equals(Router.LOCAL_SEGMENT)
					&& !helper.getSegment().equals(Router.DICO_SEGMENT)) { 
				// we return with the segmented alias in order to allow for
				// routing on segments
				uuid = alias;
			} 
		} else {
			uuid = alias;
		}
		return uuid;
	}

	/**
	 * This is the core of the routing algorithm
	 * 
	 */
	public Link route(String uuid, Message msg, Map routingTable) {

		URIHelper helper = new URIHelper(msg.getSenderEndpointUri());
		if (msg.getMessageContext().getInBoundChannel() != null) {
			// Only populate routingtable with senderSegment/senderUuid if the
			// message enters the router through an incoming channel
			String destUuid = msg.getMessageContext().getInBoundLink().getDestinationUuid();
			String curSegment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(destUuid);

			String senderEndpointUuid = helper.getEndpoint();
			String senderSegment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(senderEndpointUuid);
			if (senderSegment.equals(curSegment)) {
				// If senderEndpointUuid belongs to the same segment as the
				// incoming channel
				// populate routingtable with senderUuid
				if (!routingTable.containsKey(senderEndpointUuid)) {
					routingTable.put(senderEndpointUuid, msg.getMessageContext().getInBoundChannel().getOutLink());
				}
			} else {
				// If senderEndpointUuid not belongs to the same segment as the
				// incoming channel
				// populate routingtable with segment of the senderUuid
				if (!routingTable.containsKey(senderSegment)) {
					routingTable.put(senderSegment, msg.getMessageContext().getInBoundChannel().getOutLink());
				}
			}
		}

		Link link = routingTable.get(uuid);

		// route down
		if (link == null) {
			String toSegment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(uuid);
			Link inboundLink = msg.getMessageContext().getInBoundLink();
			if (inboundLink == null) {
				return null; // The message has not arrived this router through
				// an inboundLink. It is invalid and can not be
				// routed
			}
			String destUuid = inboundLink.getDestinationUuid();
			if (destUuid == null) {
				logger.warn(COOSInstanceName + ":destinationUuid is null on incoming link : "
								+ inboundLink.getLinkId());
			}
			String curSegment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(destUuid);

			if (!toSegment.equals(curSegment)) {
				// route down
				while (!toSegment.equals("")) {
					link = routingTable.get(toSegment);
					if (link != null) {
						break;
					} else {
						toSegment = UuidHelper.getParentSegment(toSegment);
					}
				}

				// route up
				/*if (link == null) {
					if (curSegment != null) {
						String parentSegment = UuidHelper.getParentSegment(curSegment);
						if (parentSegment != null) {
							link = routingTable.get(parentSegment);
						}
					}
				}*/
			}
		}

		// route along defaultGw if set. Must be different from the incoming link
		if (link == null) {
			if (defaultGw != null && defaultGw != msg.getMessageContext().getInBoundChannel().getOutLink()) {
				link = defaultGw;
			} else {
				if (defaultGw == null) {
					logger.warn("Defaultgw is not set!");
				} else if (defaultGw == msg.getMessageContext().getInBoundChannel().getOutLink()) {
					logger.warn("Incoming link is defaultgw!");
				}
			}
		}
		return link;
	}

	public void replyErrorReason(Message msg, String message) {
		// Only send error indication on type msg. i.e. from an endpoint
		if (msg.getHeader(Message.TYPE).equals(Message.TYPE_MSG)) {
			msg.setReceiverEndpointUri(msg.getSenderEndpointUri());
			msg.setHeader(Message.TYPE, Message.TYPE_ERROR);
			msg.setHeader(Message.ERROR_REASON, message);
			processMessage(msg);
		}
	}

	public Processor getDefaultProcessor() {
		return this;
	}

	/**
	 * Adding a link to the router. Can either be a link to an endpoint/router
	 * or a link to another segment
	 * 
	 * @param routerUuid
	 * @param link
	 * @throws ConnectingException
	 */
	public void addLink(String routerUuid, Link link) throws ConnectingException {

		if (UuidHelper.isUuid(routerUuid)) {
			// It is a router or endpoint
			String seg = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(routerUuid);
			RoutingAlgorithm algorithm = routingAlgorithms.get(seg);
			if (algorithm == null) {
				throw new ConnectingException("Router is not attached to segment: " + seg);
			}
			RouterSegment rs = segmentMapping.get(seg);
			rs.setTimestamp(0);
			
			link.setDestinationUuid(routerUuid);
			links.put(link.getLinkId(), link);
			
			if (link.getChannel() != null && link.getChannel().isDefaultGw()) {
				defaultGw = link;
				logger.debug("Setting defaultgw " + link);
			}
			algorithm.publishLink(link);
		} else {
			// it is a segment
			link.setDestinationUuid(routerUuid);
			links.put(link.getLinkId(), link);
			routingAlgorithms.get(routerUuid).publishLink(link);
		}
		logger.debug(getCOOSInstanceName() + ": Adding link: " + link);
	}

	public Link getLink(String destinationUuid) {
		if (destinationUuid != null) {
			for (Link link : links.values()) {
				if (link.getDestinationUuid().equals(destinationUuid)) {
					return link;
				}
			}
		}
		return null;
	}

	public void removeLinkById(String linkId) {
		Link link = links.get(linkId);
		if (link != null) {
			link.setCost(LinkCost.MAX_VALUE);

			// remove from routing table can be done here
			for (Map routingTable : routingTables.values()) {
				for (String key : routingTable.keySet()) {
					Link l = routingTable.get(key);
					if (l != null && l.equals((link))) {
						routingTable.remove(key);
					}
				}
			}
		}

	}

	public void removeLink(String destinationUuid) {
		for (Link link : links.values()) {
			if (link.getDestinationUuid().equals(destinationUuid)) {
				link.setCost(LinkCost.MAX_VALUE);
				routingAlgorithms.get(UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(destinationUuid))
						.publishLink(link);

				// remove from routing table can be done here
				for (Map routingTable : routingTables.values()) {
					for (String key : routingTable.keySet()) {
						if (routingTable.get(key).equals((link))) {
							routingTable.remove(key);
						}
					}
				}
			}
		}
	}

	public void addQoSClass(String QoSClass, boolean isDefaultQoSClass) {
		this.QoSClasses.add(QoSClass);
		if (defaultQoSClass == null || isDefaultQoSClass) {
			defaultQoSClass = QoSClass;
		}

		// create routing tables
		if (routingTables.get(QoSClass) == null) {
			routingTables.put(QoSClass, new ConcurrentHashMap());
		}
	}

	public Collection getQoSClasses() {
		return this.QoSClasses;
	}

	public void addPreProcessor(RouterProcessor preProcessor) {
		preProcessor.setRouter(this);
		preProcessors.add(preProcessor);
	}

	public void addPostProcessor(RouterProcessor postProcessor) {
		postProcessor.setRouter(this);
		postProcessors.add(postProcessor);
	}

	public String getCOOSInstanceName() {
		return this.COOSInstanceName;
	}

	public void setCOOSInstanceName(String COOSInstanceName) {
		this.COOSInstanceName = COOSInstanceName;

	}

	public void setCOOS(COOS coos) {
		this.COOSInstanceName = coos.getName();
		this.COOS = coos;
	}

	public COOS getCOOS() {
		return COOS;
	}

	public void setSegmentMappings(Hashtable segmentMapping) {
		this.segmentMapping = new ConcurrentHashMap(segmentMapping);
	}

	public void addSegmentMapping(String segment, String routerUUID, String routerAlgorithm) {
		this.segmentMapping.put(segment, new RouterSegment(segment, routerUUID, routerAlgorithm, false));
	}

	public synchronized void addRoutingAlgorithm(String routerUuid, RoutingAlgorithm routingAlgorithm) {
		if (!UuidHelper.isRouterUuid(routerUuid)) {
			throw new IllegalArgumentException("Router uuid must start with prefix " + ROUTER_UUID_PREFIX);
		}

		Processor processor = null;
		
		this.routingAlgorithms.put(UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(routerUuid), routingAlgorithm);
		routerUuids.add(routerUuid);

		if (routerUuids.size() > 1) {
			for (String uuid : routerUuids) {
				if (!routerUuid.equals(uuid)) {
					Link link = new Link(0);
					link.addFilterProcessor(processor);
					link.setChainedProcessor(this);
					try {
						addLink(UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(uuid), link);
					} catch (Exception e) {
						e.printStackTrace();
					}

					link = new Link(0);
					link.addFilterProcessor(processor);
					link.setChainedProcessor(this);
					try {
						addLink(UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(routerUuid), link);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}

		if (running) {
			try {
				routingAlgorithm.start();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	public void aliasesUpdated() {

		// remove global aliases
		for (Link link : links.values()) {
			Collection aliases = link.getAlises();
			synchronized (aliases) { 
				//Synchronizes on alias since concurrent access to this member can cause exceptions, 
				//Can not use ConcurrentHashmap cause of java 1.3
				for (Iterator iterator = aliases.iterator(); iterator.hasNext();) {
					String alias = (String) iterator.next();
					if (alias.startsWith(Router.DICO_SEGMENT + ".") && !aliasTable.containsKey(alias)) {
						iterator.remove();
					}
				}
			}
		}

		// add global aliases in crossegment links
		for (String alias : aliasTable.keySet()) {
			if (alias.startsWith(Router.DICO_SEGMENT + ".")) {
				String aliasUuid = aliasTable.get(alias);
				String aliasSegment = UuidHelper.getSegmentFromEndpointNameOrEndpointUuid(aliasUuid);
				for (Link link : links.values()) {
					String segment = UuidHelper.getSegmentFromSegmentOrEndpointUuid(link.getDestinationUuid());
					if (aliasSegment.equals(segment) && UuidHelper.isSegment(link.getDestinationUuid())) {
						link.addAlias(alias);
					}
				}
			}
		}
	}

	public String getDefaultQoSClass() {
		return defaultQoSClass;
	}

	public RoutingAlgorithm getRoutingAlgorithm(String segment) {
		return routingAlgorithms.get(segment);
	}

	public Map getRoutingTables() {
		return routingTables;
	}

	public Map getRoutingTable(String qos) {
		return routingTables.get(qos);
	}

	public Map getLinks() {
		return links;
	}

	public Link getLinkById(String id) {

		return links.get(id);
	}

	public void start() throws Exception {
		if (!running && enabled) {
			for (RoutingAlgorithm routingAlgorithm : routingAlgorithms.values()) {
				routingAlgorithm.start();
			}

			running = true;
		}
	}

	public void stop() throws Exception {
		for (RoutingAlgorithm routingAlgorithm : routingAlgorithms.values()) {
			routingAlgorithm.stop();
		}

		running = false;
	}

	public String toString() {
		return "Router " + COOSInstanceName;
	}

	public Map getAliasTable() {
		return aliasTable;
	}

	public void putAlias(String alias, String toUuid) {
		String oldToUuid = aliasTable.get(alias);
		if (oldToUuid != null && !oldToUuid.equals(toUuid)) {
			logger.warn("Possible alias conflict for alias: " + alias + ". Was pointing to : " + oldToUuid
					+ ". Now pointing to :" + toUuid + ".");
		}
		aliasTable.put(alias, toUuid);
		aliasesUpdated();
	}

	public void removeAlias(String alias) {
		aliasTable.remove(alias);
		aliasesUpdated();
	}

	@Override
	public void addDynamicSegment(String segmentName, String routingAlg) throws ConnectingException {
		RoutingAlgorithm prototype = COOS.getRoutingAlgorithm(routingAlg);
		if (prototype == null) {
			throw new ConnectingException("Routingalgorithm: " + routingAlg
					+ " not defined. Refusing dynamic segment allocation: " + segmentName);
		}
		RoutingAlgorithm algorithm = prototype.copy();
		UuidGenerator uuidGenerator = new UuidGenerator(ROUTER_UUID_PREFIX);
		String routerUuid = segmentName + "." + uuidGenerator.generateSanitizedId();
		segmentMapping.put(segmentName, new RouterSegment(segmentName, routerUuid, routingAlg, false, true));
		algorithm.init(routerUuid, this);
	}

	@Override
	public RouterSegment getSegment(String segmentName) {
		return segmentMapping.get(segmentName);
	}

	@Override
	public Map getSegmentMap() {
		return segmentMapping;
	}

	@Override
	public void removeSegment(String segmentName) {
		segmentMapping.remove(segmentName);
		routingAlgorithms.remove(segmentName);
		removeLink(segmentName);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy