More UPnP Programming

Events

Timer

Interface for different implementations of timers, matches the methods defined for the timer service


/**
 * Timer service as per Timer XML description
 * This may be subclassed to provide functionality
 */
package service;

import xmltypes.*;
import org.cybergarage.upnp.Service;

public interface Timer {
 
    public void setTime(Time t);

    public Time getTime();

    public boolean isValidTime();

// Change start public void setService(Service svc);
// Change end }

Ticker Timer

Another implementation that runs a "ticker" in a thread to update the time. This timer is invalid until something else sets its time


/**
 * Timer service as per Timer XML description
 *
 * The timer service has two state vbls, Time and TimeValid.
 * Time is continually changing, updated every second. If we kept this
 * state in the cybergarage state vbl (which is a string) then every
 * second we would be parsing the string into a Time object, incrementing
 * it and then turning it back into a string. Kind of wasteful. So it is
 * (maybe) better to keep the state in a Time object (in the Ticker) and 
 * just copy it into the cybergarage state vbl on each change. On the other hand,
 * TimeValid changes rarely and isn't checked much, so we can keep it
 * in the cybergarage state vbl always
 */
package service;

import xmltypes.*;
import org.cybergarage.upnp.Service;
import org.cybergarage.upnp.StateVariable;

public class TickerTimer implements Timer {

    private Ticker ticker;

// Change start private Service service; private StateVariable timeVar; private StateVariable validVar;
// Change end public TickerTimer() { }
// Change start public void setService(Service svc) { service = svc; Time time = new Time("12:00:00"); timeVar = svc.getStateVariable("Time"); timeVar.setValue(time.toString()); validVar = svc.getStateVariable("TimeValid"); validVar.setValue("false"); ticker = new Ticker(time, timeVar); ticker.start(); }
// Change end public void setTime(Time t) { if (ticker != null) { ticker.stopRunning(); }
// Change start timeVar.setValue(t.toString()); validVar.setValue("true");
// Change end ticker = new Ticker(t, timeVar); ticker.start(); } public Time getTime() { Time time = new Time(timeVar.getValue()); return time; } public boolean isValidTime() { if (validVar.getValue().equals("true")) { return true; } else { return false; } } } class Ticker extends Thread { private Time time; private boolean keepRunning = true;
// Change start private StateVariable timeVar;
// Change end
// Change start public Ticker(Time t, StateVariable tVar) { time = t; timeVar = tVar; }
// Change end public void run() { while (keepRunning) { try { sleep(1000); } catch(InterruptedException e) { } if (keepRunning) { time.increment();
// Change start timeVar.setValue(time.toString());
// Change end } } } public void stopRunning() { keepRunning = false; } }

Clock device


/******************************************************************
 *
 *	CyberUPnP for Java
 *
 *	Copyright (C) Satoshi Konno 2002
 *
 *	File : ClockDevice.java
 *
 ******************************************************************/

package device;

import service.*;
import xmltypes.*;

import java.io.*;

import org.cybergarage.upnp.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.control.*;
import org.cybergarage.http.*;
import org.cybergarage.upnp.ssdp.*;

public class ClockDevice extends Device implements ActionListener, QueryListener, NotifyListener 
{
    private final static String DESCRIPTION_FILE_NAME = "description/description.xml";
    private final static String PRESENTATION_URI = "clock/presentation";

    private ControlPoint ctrlPoint = new ControlPoint();
    
    private Timer timer;
    private Service timerService;

    private Device lastDeviceSeen;

    public ClockDevice() throws InvalidDescriptionException
    {
	super(new File(DESCRIPTION_FILE_NAME));

	
	UPnP.setEnable(UPnP.USE_ONLY_IPV4_ADDR);

	org.cybergarage.util.Debug.on();

	ctrlPoint.addNotifyListener(this);
	ctrlPoint.start();
	
	Action getTimeAction = getAction("GetTime");
	getTimeAction.setActionListener(this);
	
	Action setTimeAction = getAction("SetTime");
	setTimeAction.setActionListener(this);
	
	Action timeValidAction = getAction("TimeValid");
	timeValidAction.setActionListener(this);
	
	ServiceList serviceList = getServiceList();
	// there should only be a timer service
	timerService = serviceList.getService(0);
	timerService.setQueryListener(this);
	
	setLeaseTime(60);
    }

    public void setTimer(Timer t) {
	timer = t;
	System.out.println("Timer " + timer + " service " + timerService);
	// *****************************************
	// This is the only line that really changed
	// *****************************************
// Change start timer.setService(timerService);
// Change end System.out.println("Our timer service is " + t); } //////////////////////////////////////////////// // ActionListener //////////////////////////////////////////////// public boolean actionControlReceived(Action action) { String actionName = action.getName(); if (actionName.equals("GetTime") == true) { System.out.println("GetTime action control"); Time now = timer.getTime(); Argument timeArg = action.getArgument("CurrentTime"); timeArg.setValue(now.toString()); return true; } if (actionName.equals("SetTime") == true) { System.out.println("SetTime action control"); Argument timeArg = action.getArgument("NewTime"); String newTime = timeArg.getValue(); timer.setTime(new Time(newTime)); System.out.println("SetTime action to " + newTime); return true; } if (actionName.equals("TimeValid") == true) { System.out.println("TimeValid action control"); boolean timeValid = timer.isValidTime(); Argument timeArg = action.getArgument("Valid"); timeArg.setValue("" + timeValid); return true; } return false; } //////////////////////////////////////////////// // QueryListener ////////////////////////////////////////////////
// Change start public boolean queryControlReceived(StateVariable stateVar) { System.out.println("query control"); if (stateVar.getName().equals("Time")) { stateVar.setValue(getTime().toString()); return true; } else if (stateVar.getName().equals("TimeValid")) { stateVar.setValue("" + isValidTime()); return true; } return false; }
// Change end //////////////////////////////////////////////// // HttpRequestListner //////////////////////////////////////////////// public void httpRequestRecieved(HTTPRequest httpReq) { System.out.println("HTTP request: " + httpReq); String uri = httpReq.getURI(); if (uri.startsWith(PRESENTATION_URI) == false) { super.httpRequestRecieved(httpReq); return; } } public void setTime(Time time) { System.out.println("setTime in clock device to " + time); timer.setTime(time); } public Time getTime() { return timer.getTime(); } public boolean isValidTime() { return timer.isValidTime(); } public void deviceNotifyReceived(SSDPPacket ssdpPacket) { System.out.println("New SSDPPacket, all " + ssdpPacket); /* There doesn;t seem to be a simple way of getting the new * device from the packet, even though CyberLink creates a * new device from it - and adds it to a list. Looks like we * have to get the whole list and search through it for the * device created by this packet :-( */ DeviceList devList = ctrlPoint.getDeviceList(); Device dev = null; System.out.println("Devices: " + devList.size()); for (int n = 0; n < devList.size(); n++) { if (devList.getDevice(n).getSSDPPacket() == ssdpPacket) { System.out.println("Found the new device matching SSDP packet"); dev = devList.getDevice(n); break; } } if (dev == null) { // couldn't find it? That shouldn't happen... return; } ServiceList timerServices = new ServiceList(); if (dev.getDeviceType().equals("urn:schemas-upnp-org:device:clock:1")) { ServiceList services = dev.getServiceList(); for (int m = 0; m < services.size(); m++) { Service svc = services.getService(m); if (svc.getServiceType().equals("urn:schemas-upnp-org:service:timer:1")) { timerServices.add(svc); } } } tryClockValidation(timerServices); } private ServiceList getTimerServices() { ServiceList timerServices = new ServiceList(); DeviceList devList = ctrlPoint.getDeviceList(); System.out.println("Devices: " + devList.size()); for (int n = 0; n < devList.size(); n++) { Device dev = devList.getDevice(n); if (dev.getDeviceType().equals("urn:schemas-upnp-org:device:clock:1")) { ServiceList services = dev.getServiceList(); for (int m = 0; m < services.size(); m++) { Service svc = services.getService(m); if (svc.getServiceType().equals("urn:schemas-upnp-org:service:timer:1")) { timerServices.add(svc); } } } } return timerServices; } private void tryClockValidation(ServiceList services) { org.cybergarage.util.Debug.on(); for (int n = 0; n < services.size(); n++) { Service svc = (Service) services.elementAt(n); System.out.println("Checking service " + svc); Action validAction = svc.getAction("TimeValid"); if (validAction.postControlAction()) { System.out.println("Checking TimeValid succeeded"); Argument arg = validAction.getArgument("Valid"); String value = arg.getValue(); System.out.println("valid time? " + value); if (isValidTime() && value.equals("false")) { // other clock needs to be set Action setTimeAction = svc.getAction("SetTime"); setTimeAction.setArgumentValue("NewTime", getTime().toString()); setTimeAction.postControlAction(); } else if (! isValidTime() && value.equals("true")) { Action getTimeAction = svc.getAction("GetTime"); if ( ! getTimeAction.postControlAction()) { System.out.println("GetTime post action failed"); } Argument arg2 = getTimeAction.getArgument("CurrentTime"); Object val = arg2.getValue(); System.out.println("Arg type " + val.getClass()); System.out.println("setTime in clock validation to " + arg2.getValue()); setTime(new Time(arg2.getValue())); } } } } }

Listener

This will listen to events and print them


/**
 * Listen for state changes in clocks
 */

package client;

import service.*;
import xmltypes.*;

import java.io.*;

import org.cybergarage.upnp.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.control.*;
import org.cybergarage.http.*;
import org.cybergarage.upnp.ssdp.*;
import org.cybergarage.upnp.event.*;

public class ValidityListener implements EventListener, NotifyListener 
{
    private ControlPoint ctrlPoint = new ControlPoint();
    
    private StateVariable timeVar;
    private Device lastDeviceSeen;

    public static void main(String[] args) {
	ValidityListener client = new ValidityListener();
	
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		// Wait for a "notify" from another thread
		// that will never be sent.
		// So we stay alive for ever
		keepAlive.wait();
	    } catch(java.lang.InterruptedException e) {
		// do nothing
	    }
	}
    }

    public ValidityListener()
    {
	UPnP.setEnable(UPnP.USE_ONLY_IPV4_ADDR);

	// org.cybergarage.util.Debug.on();

	ctrlPoint.addNotifyListener(this);

// Change start ctrlPoint.addEventListener(this);
// Change end ctrlPoint.start(); }
// Change start public void eventNotifyReceived(String uuid, long seq, String varName, String value) { // a new event has arrived System.out.println("UUID: \"" + uuid + "\""); System.out.println("Name: \"" + varName + "\""); System.out.println("Value: \"" + value + "\""); }
// Change end public void deviceNotifyReceived(SSDPPacket ssdpPacket) { DeviceList devList = ctrlPoint.getDeviceList(); Device dev = null; System.out.println("Devices: " + devList.size()); for (int n = 0; n < devList.size(); n++) { if (devList.getDevice(n).getSSDPPacket() == ssdpPacket) { System.out.println("Found the new device matching SSDP packet"); dev = devList.getDevice(n); break; } } if (dev == null) { // couldn't find it? That shouldn't happen... return; } if (dev.getDeviceType().equals("urn:schemas-upnp-org:device:clock:1")) { ServiceList services = dev.getServiceList(); for (int m = 0; m < services.size(); m++) { Service svc = services.getService(m); if (svc.getServiceType().equals("urn:schemas-upnp-org:service:timer:1")) { System.out.println("Subscribing to timer events");
// Change start // subscribe to events ctrlPoint.subscribe(svc);
// Change end } } } } }

Compile and run


Remote UI

UPnP remote UI

Cybergarage remote UI

Remote UI ClockDevice


/******************************************************************
 *
 *	CyberUPnP for Java
 *
 *	Copyright (C) Satoshi Konno 2002
 *
 *	File : ClockDevice.java
 *
 ******************************************************************/

package device;

import service.*;
import xmltypes.*;

import java.io.*;

import org.cybergarage.upnp.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.control.*;
import org.cybergarage.http.*;
import org.cybergarage.upnp.ssdp.*;

public class ClockDevice extends Device implements ActionListener, QueryListener, NotifyListener 
{
    private final static String DESCRIPTION_FILE_NAME = "description/description.xml";
// Change start private final static String PRESENTATION_URI = "/presentation"; private final static String REFRESH_UI = "/refresh";
// Change end private ControlPoint ctrlPoint = new ControlPoint(); private Timer timer; private Service timerService; private Device lastDeviceSeen; public ClockDevice() throws InvalidDescriptionException { super(new File(DESCRIPTION_FILE_NAME)); UPnP.setEnable(UPnP.USE_ONLY_IPV4_ADDR); org.cybergarage.util.Debug.on(); ctrlPoint.addNotifyListener(this); ctrlPoint.start(); Action getTimeAction = getAction("GetTime"); getTimeAction.setActionListener(this); Action setTimeAction = getAction("SetTime"); setTimeAction.setActionListener(this); Action timeValidAction = getAction("TimeValid"); timeValidAction.setActionListener(this); ServiceList serviceList = getServiceList(); // there should only be a timer service timerService = serviceList.getService(0); timerService.setQueryListener(this); setLeaseTime(60); } public void setTimer(Timer t) { timer = t; System.out.println("Our timer service is " + t); } //////////////////////////////////////////////// // ActionListener //////////////////////////////////////////////// public boolean actionControlReceived(Action action) { String actionName = action.getName(); if (actionName.equals("GetTime") == true) { System.out.println("GetTime action control"); Time now = timer.getTime(); Argument timeArg = action.getArgument("CurrentTime"); timeArg.setValue(now.toString()); return true; } if (actionName.equals("SetTime") == true) { System.out.println("SetTime action control"); Argument timeArg = action.getArgument("NewTime"); String newTime = timeArg.getValue(); timer.setTime(new Time(newTime)); System.out.println("SetTime action to " + newTime); return true; } if (actionName.equals("TimeValid") == true) { System.out.println("TimeValid action control"); boolean timeValid = timer.isValidTime(); Argument timeArg = action.getArgument("Valid"); timeArg.setValue("" + timeValid); return true; } return false; } //////////////////////////////////////////////// // QueryListener //////////////////////////////////////////////// public boolean queryControlReceived(StateVariable stateVar) { System.out.println("query control"); if (stateVar.getName().equals("Time")) { stateVar.setValue(getTime().toString()); return true; } else if (stateVar.getName().equals("TimeValid")) { stateVar.setValue("" + isValidTime()); return true; } return false; } // // HttpRequestListner // // This is responsible for generating the HTML UI // public void httpRequestRecieved(HTTPRequest httpReq) { System.out.println("HTTP request: " + httpReq); String uri = httpReq.getURI();
// Change start if (uri.startsWith(PRESENTATION_URI)) { sendPage(httpReq); } else if (uri.startsWith(REFRESH_UI)) { sendPage(httpReq); } else { super.httpRequestRecieved(httpReq); return; }
// Change end }
// Change start private void sendPage(HTTPRequest httpReq) { // deliver an HTML page String time = getTime().toString(); String refreshUrl = "http://" + httpReq.getLocalAddress() + ":" + httpReq.getLocalPort() + REFRESH_UI; System.out.println("referesh url " + refreshUrl); // the page attempts to turn caching off at the client side // but a bug in JEditorPane ignores the Cache-Control value String page = "<html>\n" + "<head>\n" + "</head>\n" + "<body>\n" + "<form method=\"post\" action=\"" + refreshUrl + "\">\n" + "New time" + "<input type=\"text\" name=\"time\">\n" + "<input type=\"submit\">\n" + "<p>\n" + time + "\n<br>\n" + "<a href=\"" + refreshUrl + "\"> Refresh </a>\n" + "</p>\n" + "</body>\n" + "</html>\n"; System.out.println("Sending page " + page); HTTPResponse response = new HTTPResponse(); response.setStatusCode(HTTPStatus.OK); response.setContent(page); response.setCacheControl(0); // no caching - ignored by JEditorPane httpReq.post(response); }
// Change end public void setTime(Time time) { System.out.println("setTime in clock device to " + time); timer.setTime(time); } public Time getTime() { return timer.getTime(); } public boolean isValidTime() { return timer.isValidTime(); } public void deviceNotifyReceived(SSDPPacket ssdpPacket) { System.out.println("New SSDPPacket, all " + ssdpPacket); /* There doesn;t seem to be a simple way of getting the new * device from the packet, even though CyberLink creates a * new device from it - and adds it to a list. Looks like we * have to get the whole list and search through it for the * device created by this packet :-( */ DeviceList devList = ctrlPoint.getDeviceList(); Device dev = null; System.out.println("Devices: " + devList.size()); for (int n = 0; n < devList.size(); n++) { if (devList.getDevice(n).getSSDPPacket() == ssdpPacket) { dev = devList.getDevice(n); break; } } if (dev == null) { // couldn't find it? That shouldn't happen... return; } /* this doesn't work :-( device changes each time if (dev == lastDeviceSeen) { // UPnP sends repeat messages to try to make sure things // get through. But we don't want to repeat work System.out.println("Seen this device before"); lastDeviceSeen = dev; return; } */ ServiceList timerServices = new ServiceList(); if (dev.getDeviceType().equals("urn:schemas-upnp-org:device:clock:1")) { ServiceList services = dev.getServiceList(); for (int m = 0; m < services.size(); m++) { Service svc = services.getService(m); if (svc.getServiceType().equals("urn:schemas-upnp-org:service:timer:1")) { timerServices.add(svc); } } } tryClockValidation(timerServices); } private ServiceList getTimerServices() { ServiceList timerServices = new ServiceList(); DeviceList devList = ctrlPoint.getDeviceList(); System.out.println("Devices: " + devList.size()); for (int n = 0; n < devList.size(); n++) { Device dev = devList.getDevice(n); if (dev.getDeviceType().equals("urn:schemas-upnp-org:device:clock:1")) { ServiceList services = dev.getServiceList(); for (int m = 0; m < services.size(); m++) { Service svc = services.getService(m); if (svc.getServiceType().equals("urn:schemas-upnp-org:service:timer:1")) { timerServices.add(svc); } } } } return timerServices; } private void tryClockValidation(ServiceList services) { org.cybergarage.util.Debug.on(); for (int n = 0; n < services.size(); n++) { Service svc = (Service) services.elementAt(n); System.out.println("Checking service " + svc); Action validAction = svc.getAction("TimeValid"); if (validAction.postControlAction()) { System.out.println("Checking TimeValid succeeded"); Argument arg = validAction.getArgument("Valid"); String value = arg.getValue(); System.out.println("valid time? " + value); if (isValidTime() && value.equals("false")) { // other clock needs to be set Action setTimeAction = svc.getAction("SetTime"); setTimeAction.setArgumentValue("NewTime", getTime().toString()); setTimeAction.postControlAction(); } else if (! isValidTime() && value.equals("true")) { Action getTimeAction = svc.getAction("GetTime"); if ( ! getTimeAction.postControlAction()) { System.out.println("GetTime post action failed"); } Argument arg2 = getTimeAction.getArgument("CurrentTime"); Object val = arg2.getValue(); System.out.println("Arg type " + val.getClass()); System.out.println("setTime in clock validation to " + arg2.getValue()); setTime(new Time(arg2.getValue())); } } } } }

Remote UI client


/**
 * Listen for state changes in clocks
 */

package client;

import service.*;
import xmltypes.*;

import java.io.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.Document;
import java.awt.*;

import org.cybergarage.upnp.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.control.*;
import org.cybergarage.http.*;
import org.cybergarage.upnp.ssdp.*;
import org.cybergarage.upnp.event.*;

import java.net.URL;
import java.net.HttpURLConnection;

public class UI extends JFrame implements EventListener, NotifyListener 
{
    private ControlPoint ctrlPoint = new ControlPoint();
    private DeviceList knownDevices = new DeviceList();
    private StateVariable timeVar;

    private FlowLayout layout = new FlowLayout();
    private Container contentPane = getContentPane();

    public static void main(String[] args) {
	UI client = new UI();
    }

    public UI()
    {
	UPnP.setEnable(UPnP.USE_ONLY_IPV4_ADDR);

	org.cybergarage.util.Debug.on();

	ctrlPoint.addNotifyListener(this);
	ctrlPoint.addEventListener(this);
	ctrlPoint.start();

	contentPane.setLayout(layout);
	setSize(200, 200);
	setVisible(true);
    }

    public void eventNotifyReceived(String uuid, long seq, 
				    String varName, String value) {
	System.out.println("UUID: \"" +  uuid + "\"");
	System.out.println("Name: \"" +  varName + "\"");
	System.out.println("Value: \"" +  value + "\"");
    }


    public void deviceNotifyReceived(SSDPPacket ssdpPacket) { 
	if ( ! ssdpPacket.getNTS().equals("ssdp:alive")) {
	    return;
	}

	/* This code cleaned up to avoid repeating
	 * service finds, but no changes of relevance
	 * to UI
	 */

	/* We can get multiple packets for one device from multiple
	 * UDP packets sent in case one is lost. Also, we get
	 * rebroadcasts every few minutes. This is tedious
	 */

	Device dev = getDeviceFromPacket(ssdpPacket);
	if (dev == null) {
	    // This can happen if the packet describes a service, 
	    // a device, but not a ROOT device
	    return;
	}

	// process the packet if it is for a new device
	if (isNewDevice(dev)) {
	    if (dev.getDeviceType().equals("urn:schemas-upnp-org:device:clock:1")) {
// Change start // The presentation URL is relative e.g. /presentation String presentationURL = dev.getPresentationURL(); System.out.println("Presentation url is \"" + presentationURL + "\""); // the URLBase is the http://hostname String urlBase = dev.getURLBase(); System.out.println("Adding new device " + dev); System.out.println("From packet " + ssdpPacket); addClock(/* urlBase + */ presentationURL);
// Change end } } else { System.out.println("Seen this device before " + dev); } } /* New code, just cleans up old stuff * No changes for UI */ private Device getDeviceFromPacket(SSDPPacket packet) { DeviceList devList = ctrlPoint.getDeviceList(); for (int n = 0; n < devList.size(); n++) { Device dev = devList.getDevice(n); if (dev.getSSDPPacket() == packet) { return dev; } } // this will happen for services/embedded devices etc System.out.println("No device found for packet " + packet); return null; } /* New code, just cleans up old stuff * No changes for UI */ private boolean isNewDevice(Device dev) { System.out.println("UDN " + dev.getUDN()); for (int n = 0; n < knownDevices.size(); n++) { if (dev.getUDN().equals(knownDevices.getDevice(n).getUDN())) { return false; } } knownDevices.add(dev); return true; }
// Change start has to get UI from device and display it // Display is done here in a JEditPane private void addClock(String url) { // get HTML page from URL, put it in a component and add it to this System.out.println("URL is " + url); /* OR: use Runtime.exec() to start IE,Netscape, etc * and pass it the URL */ /* JEditorPane DOES handle forms properly after all! */ final JEditorPane pane = new JEditorPane(); try { pane.setPage(url); pane.setEditable(false); // or we can't follow hyperlinks System.out.println("New page contents " + pane.getText()); contentPane.add(pane); } catch(IOException e) { e.printStackTrace(); } // listen for hyperlink events pane.addHyperlinkListener(new HyperlinkListener() { // user over/clicking Refresh hyperlink public void hyperlinkUpdate(HyperlinkEvent e) { java.net.URL url = e.getURL(); // Turn caching off! // Due to a bug in JEditorPane, this doesn't work HttpURLConnection uc = null; try { uc = (HttpURLConnection)url.openConnection(); } catch(IOException ioe) { ioe.printStackTrace(); return; } uc.setUseCaches(false); if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { try { // url was clicked on - we should reload the page // WORKAROUND for JEditorPane bug always caching // url even if it's contents have changed, or // Expires has happened or Cache-Control set to // "no-cache", etc // Clear the internal URL object in this control // TO PREVENT URL CACHING... Document doc = pane.getDocument(); doc.putProperty(Document.StreamDescriptionProperty,null); // Sun know of this bug, and just promise to fix the doco :-( // now reload it pane.setPage(url); } catch(IOException ioe) { ioe.printStackTrace(); } } } } ); }
// Change end }

Compile and run


Jan Newmarch (http://jan.newmarch.name)
jan@newmarch.name
Last modified: Wed May 26 20:55:11 EST 2004
Copyright ©Jan Newmarch
Copyright © Jan Newmarch, Monash University, 2007
Creative Commons License This work is licensed under a Creative Commons License
The moral right of Jan Newmarch to be identified as the author of this page has been asserted.