Plug into Java with Java Plug-in, Part 2

Use Java Plug-in with the Firefox Web browser

Six years have passed since I wrote "Plug into Java with Java Plug-in" for JavaWorld. My earlier article defined Java Plug-in, showed how to install version 1.2 for Netscape Communicator 4.5 and Internet Explorer 3.02, presented Windows registry settings related to Java Plug-in, explored Java Plug-in's control panel, examined two Java consoles, discussed the <embed> and <object> HTML tags, and revealed a Swing-based demonstration applet.

Many versions of Java and Java Plug-in have emerged since I wrote that article. And since it's showing its age, I've created a sequel that focuses on one of the more recent Java Plug-ins in the context of the Firefox Web browser. This article first shows you how to access Java Plug-in from Firefox. It next examines the Java Plug-in Document Object Model (DOM), applet state persistence, and cookies. Various applets, run from Firefox, reinforce the knowledge you acquire while studying those topics. The article concludes by exploring the link between Firefox and Java Plug-in. Because there is more to Java Plug-in than what I cover in this article, I recommend studying Sun's Java Plug-in documentation to learn more about that technology.

I base this article's material on Mozilla Firefox 1.0, J2SE 5.0, and Windows 98 SE. Even if you don't have this software, I still recommend reading the article. Hopefully, you will discover some interesting material that is new to you.

Java Plug-in and Firefox

Java Plug-in serves as a bridge between a browser and an external Java Runtime Environment (JRE). Java Plug-in is essential for running applets in Firefox because the browser doesn't provide a JRE—including a JVM—of its own. Because this article presents applets that demonstrate different Java Plug-in topics, and because I run these applets in a Firefox context, we should review how to access Java Plug-in from Firefox. For starters, make sure that J2SE 5.0 and JRE are installed on your platform. On my Windows platform, I installed both J2SE and JRE from the jdk-1_5_0-windows-i586.exe installation file. Make sure to include the JRE in the installation because the JRE includes Java Plug-in.

After installing J2SE 5.0, connect Firefox to Java Plug-in: first select Options from Firefox's Tools menu, next select Web Features, and finally check the Enable Java checkbox.

Traditionally, the <embed> and <object> tags had to be specified to run applets under Java Plug-in. This is not necessary with Java Plug-in 5.0, which can run applets specified by the simpler <applet> tag. During installation, you are given the opportunity to have the Internet Explorer and Mozilla (Firefox)/Netscape Web browsers hand off applet execution to Java Plug-in when they encounter the <applet> tag. If you forego this option, you can make the change later via the Java Plug-in control panel. For example, start the control panel and select the Advanced tab. Then click the plus sign to the left of <APPLET> Tag Support and check the box marked Mozilla and Netscape. Close the control panel and Firefox passes execution to Java Plug-in whenever it encounters the <applet> tag. Figure 1 reveals the needed change.

Figure 1. Check Mozilla and Netscape so Firefox can pass applet execution to Java Plug-in when it encounters the <applet> tag
Note
I've discovered that checking Mozilla and Netscape is not necessary on my platform. My Firefox Web browser passes execution to Java Plug-in when it encounters <applet>, regardless of that checkbox setting. The Java Plug-in 5.0 documentation has this to say about that strange behavior: "If Mozilla and Netscape 7 are both installed and <APPLET> tag support for Mozilla and Netscape is disabled (unchecked), <APPLET> tags continue to run with the Sun VM. This is a bug that is related to the autoscan feature of Netscape 7." Because I also have Netscape 6.2.3 installed, I suppose I've found that bug. Keep this in mind if you encounter similar behavior on your platform.

Platforms can host multiple JREs. Which JRE does Java Plug-in use when Firefox encounters an

<applet>

tag? Find the answer from the Java tab on Java Plug-in's control panel. Alternatively, my version-detection applet presents that answer and proves that Firefox is connected to Java Plug-in. Figure 2 reveals 1.5.0 to be the JRE version that I am using. (Version 1.5.0 is the internal version number for J2SE 5.0.)

Figure 2. I've configured Java Plug-in to use JRE 1.5.0

Listing 1 presents this applet's VersionDemo.java source code. The source code determines the JRE version from the java.version system property.

Listing 1. VersionDetect.java

 

// VersionDetect.java

import java.awt.*;

public class VersionDetect extends java.applet.Applet { public void paint (Graphics g) { int width = getSize ().width; int height = getSize ().height;

g.setColor (Color.orange); g.fillRect (0, 0, width, height);

String version = "JRE " + System.getProperty ("java.version");

FontMetrics fm = g.getFontMetrics ();

g.setColor (Color.black); g.drawString (version, (width-fm.stringWidth (version))/2, height/2); } }

For brevity, I don't show the HTML for running this or any other applet presented in this article. Consult the source code that accompanies this article for that HTML.

Note
Firefox only supports Java Plug-in 1.3.0_01 and higher.

Contact the DOM

According to the World Wide Web Consortium, the Document Object Model (DOM) is an API "for valid HTML and well-formed XML documents. It defines the logical structure of documents and the way a document is accessed and manipulated." Java Plug-in offers two ways to contact the DOM: the netscape.javascript.JSObject class and the Common DOM API.

The JSObject interfaces between a Java applet and a Web browser's JavaScript implementation, including JavaScript objects that expose the DOM. Examples of those objects: Document, Link, and Window. I won't delve into JSObject because I discussed that class in my earlier "Talk with Me Java" article. Rather, I focus on the Common DOM API for traversing the DOM from within an applet.

The Common DOM API, introduced in J2SE version 1.4, is a collection of classes and interfaces that lets applets traverse DOM instances. Because a browser can present multiple documents via frames and windows, many DOM instances (one per document) are available for traversal. An applet typically traverses its own DOM instance—the DOM instance associated with the document identifying the applet.

The starting point for accessing a DOM instance is the static getService(Object obj) method, which is located in the com.sun.java.browser.dom.DOMService class. Typically, an invoking applet's this reference passes as an argument to getService(). That method either returns a DOMService object that interfaces between the applet and its DOM instance or throws an exception: com.sun.java.browser.dom.DOMUnsupportedException (the DOM service is not available to the object) or com.sun.java.browser.dom.DOMAccessException (a security violation has resulted). Example: DOMService service = DOMService.getService (this);.

Because Web browsers provide different DOM implementations, access to a DOM instance is not thread safe, unless that access occurs on the DOM access dispatch thread. To ensure thread safety, DOMService provides two methods that guarantee access only on the DOM access dispatch thread—invokeLater() and invokeAndWait(). Either method takes a com.sun.java.browser.dom.DOMAction interface object argument that presents a run() method. invokeLater() executes run() asynchronously on the DOM access dispatch thread, whereas invokeAndWait() executes run() synchronously on the DOM access dispatch thread.

The run() method takes a com.sun.java.browser.dom.DOMAccessor interface object as its single argument and returns a generic Object. The run() method often invokes DOMAccessor's getDocument() method, passing a reference to the applet as getDocument()'s Object argument. In reply, getDocument() returns an org.w3c.dom.Document interface object. That object's methods return information about the document. For HTML documents, cast from Document to HTMLDocument (located in org.w3c.dom.html) and invoke HTMLDocument's methods to obtain the document's title, domain, collection of applets, and more. After invoking appropriate methods, run() returns documentation information (such as an HTML document's title) as an Object. That Object subsequently returns from the invokeLater() and invokeAndWait() methods.

To clarify the discussion above, I've created a title extraction applet that extracts the title from an HTML document and displays that title to the user. That applet's TitleExtract.java source code appears in Listing 2.

Listing 2. TitleExtract.java

 

// TitleExtract.java

import com.sun.java.browser.dom.*;

import java.applet.Applet;

import java.awt.*;

import org.w3c.dom.html.*;

public class TitleExtract extends Applet { private String title = "unknown";

public void init () { try { DOMService service = DOMService.getService (this); title = (String) service.invokeAndWait (new DOMAction () { public Object run (DOMAccessor accessor) { HTMLDocument doc = (HTMLDocument) accessor.getDocument (TitleExtract.this); return doc.getTitle (); } }); } catch (DOMUnsupportedException e) { System.out.println ("DOM not supported"); } catch (DOMAccessException e) { System.out.println ("DOM cannot be accessed"); } }

public void paint (Graphics g) { int width = getSize ().width; int height = getSize ().height;

g.setColor (Color.cyan); g.fillRect (0, 0, width, height);

FontMetrics fm = g.getFontMetrics ();

g.setColor (Color.black); g.drawString (title, (width-fm.stringWidth (title))/2, height/2); } }

The DOMAction's run() method invokes HTMLDocument's getTitle() method to obtain the HTML document's title. That title subsequently displays. For example, when presented with the <title>Extract the title</title> element (excerpted from TitleExtract.html—see this article's source code), getTitle() returns Extract the title. Firefox displays this title in Figure 3.

Figure 3. A document's title is exposed
Caution
Firefox requires that the mayscript attribute be present when invoking TitleExtract from the <applet> tag. Otherwise, due to the FireFox DOM implementation's reliance on JSObject, Java Plug-in throws various exceptions.

Persist applet state across Firefox sessions

The Java Plug-in 5.0 documentation presents many interesting topics. One topic of interest is the applet persistence API, which lets an applet save its state for future access in the same Web browser session. State is saved in an internal persistence-store datastructure managed by Java Plug-in; a session begins when the Web browser starts to run and ends when the browser terminates.

The applet persistence API, introduced in J2SE 1.4, consists of three methods declared in the java.applet.AppletContext interface:

  • void setStream(String key, InputStream stream) throws IOException associates a specific key with a specific stream in the current applet context. If a stream currently maps to the key, the new stream replaces the old stream. An IOException is thrown if the size of the new stream exceeds an internal size limit.
  • InputStream getStream(String key) returns the InputStream associated with the key in the current applet context. null returns if there is no mapping for the key.
  • Iterator<String> getStreamKeys() returns an Iterator over all keys mapped to streams in the current applet context.

Save state by calling setStream(). That method stores a key (that conveniently identifies an input stream) and an InputStream reference in an internal datastructure, such as a HashMap. Restore state by calling getStream() with the identical key. That method returns an InputStream (if the key exists) to recover the state.

Because the Java Plug-in 5.0 documentation offers no example on how to use the applet persistence API, I have prepared an example. This example presents two instances of a message transfer applet, with each using the applet persistence API to send messages to the other. In Figure 4, the user enters a message in the Message To Send text field of the left instance and clicks that instance's Send button. The user then clicks the right instance's Receive button and the message appears in that instance's Message Received text field.

Figure 4. The applet persistence API lets applets transfer messages to each other. Click on thumbnail to view full-sized image.

Listing 3 presents the message transfer applet's MessageTransfer.java source code. Message transfers occur in that source code's action listener.

Listing 3. MessageTransfer.java

 

// MessageTransfer.java

import java.applet.*;

import java.awt.*;

import java.awt.event.*;

import java.io.*;

import javax.swing.*;

public class MessageTransfer extends JApplet implements ActionListener { private TextField txtRecvMsg, txtSendMsg;

public void init () { // Build the applet's GUI.

setLayout (new GridLayout (3, 1));

JPanel pnl = new JPanel (); pnl.setLayout (new FlowLayout (FlowLayout.LEFT));

pnl.add (new JLabel ("Message to send:"));

txtSendMsg = new TextField (20); pnl.add (txtSendMsg);

getContentPane ().add (pnl);

pnl = new JPanel (); pnl.setLayout (new FlowLayout (FlowLayout.LEFT));

pnl.add (new JLabel ("Message received:"));

txtRecvMsg = new TextField (20); pnl.add (txtRecvMsg);

getContentPane ().add (pnl);

pnl = new JPanel (); pnl.setLayout (new FlowLayout (FlowLayout.LEFT));

JButton btnSend = new JButton ("Send"); btnSend.addActionListener (this); pnl.add (btnSend);

JButton btnReceive = new JButton ("Receive"); btnReceive.addActionListener (this); pnl.add (btnReceive);

getContentPane ().add (pnl); }

public void actionPerformed (ActionEvent e) { JButton btn = (JButton) e.getSource ();

if (btn.getText ().equals ("Send")) { String text = txtSendMsg.getText ();

try { // Output the String object to a byte array output stream.

ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject (text); oos.close ();

// Extract the String object from the byte array output stream // as an array of bytes.

byte [] data = baos.toByteArray ();

// Convert the array of bytes to a byte array input stream. When // the setStream() method is invoked, it caches the input stream // reference and key in the applet persistent store.

InputStream is = new ByteArrayInputStream (data); getAppletContext ().setStream ("text", is); } catch (Exception e2) { System.out.println (e2.toString ()); } } else { InputStream is = getAppletContext ().getStream ("text");

if (is != null) try { // Input the cached String object.

ObjectInputStream ois = new ObjectInputStream (is); String text = (String) ois.readObject (); txtRecvMsg.setText (text); } catch (Exception e2) { System.out.println (e2.toString ()); } } } }

The message transfer applet persists a message as a String object by serializing that object to a ByteArrayOutputStream, converting that stream to a byte array, constructing a ByteArrayInputStream based on the byte array, and invoking setStream() with that stream's reference and a key named text. The applet recovers the message by invoking getStream() with text as the key, and deserializing the retrieved InputStream.

In Firefox, the persisted message is accessible to applets run from different windows or tabs. The message is not accessible to applets run from different sessions (that is, different instances of Firefox) because each session interacts with its own instance of Java Plug-in and internal persistence store datastructure.

Note
For security reasons, applets with different codebases cannot access each other's streams.

Play with cookies

Web browsers and Web servers often communicate small packets of information to each other. These packets, known as cookies, make it possible to track user preferences, automatically log users onto Websites, and so on. Java Plug-in works with Web browsers to make cookies accessible to applets. For example, Java Plug-in lets signed applets detect cookies sent from Web servers to Web browsers.

The cookie support topic in the Java Plug-in 5.0 documentation reveals several code fragments for interacting with cookies. Because you might want to play with these fragments in an applet context, and because you might be unsure how to transform these fragments into signed applets, I present a signed cookie detection applet below. After discussing source code essentials, I take you through the steps to build and sign the applet. Finally, we run that applet in the Firefox Web browser.

And now for the source code. Check out Listing 4.

Listing 4. CookieDetect.java

 

// CookieDetect.java

import java.awt.BorderLayout;

import java.awt.event.*;

import java.net.*;

import java.util.*;

import javax.swing.*;

public class CookieDetect extends JApplet implements ActionListener { private JTextArea txtaStatus; private JTextField txtURL;

public void init () { // Build the applet's GUI

JPanel pnl = new JPanel ();

pnl.add (new JLabel ("Enter URL:"));

txtURL = new JTextField (20); txtURL.addActionListener (this); pnl.add (txtURL);

getContentPane ().add (pnl, BorderLayout.NORTH);

txtaStatus = new JTextArea (10, 40);

getContentPane ().add (new JScrollPane (txtaStatus)); }

public void actionPerformed (ActionEvent e) { try { URL url = new URL (txtURL.getText ()); URLConnection conn = url.openConnection ();

conn.connect ();

Map<String, List<String>> headers = conn.getHeaderFields (); List<String> values = headers.get ("Set-Cookie");

if (values == null) { txtaStatus.setText ("No cookies detected\n"); return; }

txtaStatus.setText (""); for (Iterator iter = values.iterator (); iter.hasNext();) txtaStatus.setText (txtaStatus.getText () + iter.next () + "\n");

txtaStatus.setText (txtaStatus.getText () + "\n"); } catch (Exception e2) { System.out.println (e2); } } }

The CookieDetect.java builds a GUI consisting primarily of two items: a text field for specifying a URL, and a text area for listing cookies. The text field's action listener's actionPerformed() method is invoked whenever the user presses the Enter key and the text field has the input focus.

The actionPerformed() method first builds a URL object from the text field's URL. That method next invokes that object's openConnection() method to return a URLConnection object representing a communications link between the applet and the URL. Moving forward, URLConnection()'s connect() method establishes the link, its getHeaderFields() method obtains an unmodifiable Map of HTTP headers and values, and Map's get() method returns a List of values associated with the Set-Cookie header. A loop enumerates the values in this List; each value represents a cookie and is appended to the text area.

Assuming c:\applets\CookieDetect is the current directory and that CookieDetect.java exists in that directory, complete the following steps to build and sign the cookie detection applet:

  • Compile CookieDetect.java: javac CookieDetect.java.
  • Place the resulting CookieDetect.class classfile into a jar file: jar cvf CookieDetect.jar CookieDetect.class.
  • Create a new key in a new keystore: keytool -genkey -keystore ks -alias me. When prompted, type testtest as the keystore password, your first and last name, the name of your organizational unit (such as IT), the name of your organization, the name of your city/locality, the name of your state or province, the two-letter country code for your organizational unit, and select Yes in response to the prompt on whether the information you just entered is correct. Also, press Enter when prompted to enter the key password for me. This causes the key password to be the same as the keystore password (testtest). All of this information is stored in file ks, which we need to create a self-signed test certificate.
  • Create a self-signed test certificate: keytool -selfcert -alias me -keystore ks. When prompted, type testtest as the keystore password. The certificate is stored in ks. Alias me (in the previous step, this step, and the next step) reminds you that the certificate is self-signed and is used only for testing. In other words, don't publish applets signed by the test certificate on public Websites.
  • Sign the jar file with the self-signed test certificate: jarsigner -keystore ks CookieDetect.jar me. When prompted, type testtest as the keystore password. This tool updates the jar file's META-INF directory to contain certificate information and a digital signature for CookieDetect.class.

Let's run the applet. Extract the CookieDetect.html file from the code that accompanies this article to c:\applets\CookieDetect. That HTML file's <applet> tag includes the archive attribute to identify the CookieDetect.jar archive. Start Firefox and specify that HTML file's URL: c:\applets\CookieDetect\CookieDetect.html. After a few moments, Figure 5's security dialog box greets you.

Figure 5. The security dialog box gives you the opportunity to trust a signed applet. Click on thumbnail to view full-sized image.

Respond to the security dialog box by clicking the Yes button. The applet next appears. Specify http://javaworld.com as the URL and press Enter. As Figure 6 shows, JavaWorld's Website does not send cookies.

Figure 6. You'll get no cookies from JavaWorld

In contrast to JavaWorld, the JavaLobby Website sends a cookie. Type http://javalobby.org and Figure 7 reveals the sent cookie, whose ID is JSESSIONID.

Figure 7. The JavaLobby Website transmits a single cookie

Some Websites send multiple cookies, such as the Kasamba Website. As Figure 8 illustrates, that site transmits four cookies to Web browsers.

Figure 8. Kasamba's Website presents multiple cookies to a Web browser

I encourage you to continue playing with cookies by transforming the remaining three code fragments (from the cookie support topic in the Java Plug-in 5.0 documentation) into signed applets.

Tip
Visit http://www.cookiecentral.com/faq/ to learn more about cookies.

Under the hood

Many people have difficulty getting Firefox to recognize Java Plug-in. For one reason or another, applets won't run. You can avoid much of this difficulty by understanding how Firefox communicates with Java Plug-in. In the following sections, I reveal to you the way in which Firefox detects Java Plug-in, a JRE's NP*.dll plugin files, and something known as OJI.

Detect Java Plug-in

You've previously installed a single JRE (which contains Java Plug-in) and now install Firefox. You run that browser, surf to a Webpage that presents an applet, and the applet runs. Some time later, you wonder how Firefox found Java Plug-in so the applet could run. After all, Firefox checks its own plugins directory for plug-ins and the JRE places its Java Plug-in files in its own bin directory. Under this arrangement, how is it possible for Firefox to detect Java Plug-in?

When Firefox starts running, its directory service provider is given the task, on Windows platforms, of finding installation directories for the Adobe Acrobat, Apple Quicktime, Microsoft Windows Media Player, and Sun Java plug-ins. For Java Plug-in's installation directory to be found, the directory service provider must be passed the plugin.scan.SunJRE user preference name along with that user preference's value—the minimum version number of a JRE to locate. Furthermore, the security.enable_java user preference must exist and its Boolean value must be true.

Assuming plugin.scan.SunJRE and security.enable_java exist, and assuming security.enable_java's value is true, the directory service provider enumerates all version number subkeys of the HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Plug-in key in the Windows Registry. The highest version number, which must be greater than or equal to the version number that is the value of plugin.scan.SunJRE, identifies the JRE/Java Plug-in that Firefox uses. The version number subkey's own JavaHome subkey contains the path to the recognized JRE's home directory. The directory service provider appends \bin to this directory, and Firefox obtains the Java Plug-in installation directory. Confused? An example should eliminate the fog.

My Firefox browser specifies plugin.scan.SunJRE with a 1.3 value. That browser also specifies security.enable_java with value true. On startup, the directory service provider scans my Windows registry; the relevant settings appear below:

 HKEY_LOCAL_MACHINE
  Software
    JavaSoft
      Java Plug-in
        1.5.0
          JavaHome "C:\Program Files\Java\jre1.5.0"

The directory service provider enumerates all version number subkeys, under the Java Plug-in subkey, for the subkey with the highest version number greater than or equal to 1.3. I only have one subkey greater than or equal to 1.3: 1.5.0. Thus, the directory service provider obtains the value of the 1.5.0 subkey's JavaHome subkey—C:\Program Files\Java\jre1.5.0—and appends \bin to that value. This means that Firefox looks for Java Plug-in files in c:\Program Files\Java\jre1.5.0\bin.

Because Firefox 1.0's plugin.scan.SunJRE user preference has 1.3 as its default value, versions of Java Plug-in less than 1.3 are not recognized. This is one reason why Firefox only supports Java Plug-in 1.3.0_01 and higher.

Those NP*.dll files

Look inside your JRE's bin directory and you'll discover several files beginning with NP and ending with .dll. For example, my platform presents the following files:

 NPJava11.dll
NPJava12.dll
NPJava13.dll
NPJava14.dll
NPJava32.dll
NPJPI150.dll
NPOJI610.dll

The files listed above are Netscape plug-in files. Together with other NP*.dll files that you might discover in your JRE's bin directory, and several jpi*.dll files that should also be present, they collectively form Java Plug-in. Each NP*.dll file has the same size, because it does essentially the same thing: it works with one (or more) of the jpi*.dll files to load the virtual machine and get Java up and running.

Each NP*.dll file identifies one or more MIME (Multipurpose Internet Mail Extensions) types. These MIME types tell Firefox to load a specific NP*.dll file when that MIME type is encountered. For example, consider the following <EMBED> tag:

 <embed code="VersionDetect.class" width="200" height="200"
       pluginspage="."
       type="application/x-java-applet;version=1.1">

Notice the type attribute's application/x-java-applet;version=1.1 MIME type. When Firefox encounters this MIME type, it chooses the corresponding NP*.dll file. Because NPJava11.dll identifies that MIME type, NPJava11.dll loads—no other NP*.dll files load. NPJava11.dll works with the jpi*.dll files to get Java up and running.

When Firefox encounters the <applet> tag, which MIME type does that tag represent? I believe application/x-java-vm is the MIME type: experiments show that only NPOJI610.dll, which has that MIME type, loads when Firefox encounters <applet>.

What is OJI?

You might have noticed OJI in NPOJI610.dll, and wondered what those three letters represent. OJI stands for Open JVM Integration, a Mozilla project/API that allows external Java virtual machines to plug into a Mozilla browser like Firefox. In addition to not being tied to internal virtual machines, the browser is not tied (through the NP*.dll plug-ins) to Sun-specific external virtual machines—the browser can communicate with virtual machines from other vendors, as long as those virtual machines support OJI.

OJI has many capabilities, which include allowing the user to display the Java console through the Web browser. Also, OJI modifies an applet's lifecycle: whenever an applet's Webpage is entered, the applet's init() and start() methods invoke; whenever that page is exited, the stop() and destroy() methods invoke. To see this for yourself, compile Listing 5's LifeCycle.java source code, run the applet in Firefox, open the Java console, and switch back and forth between pages. You will also notice a call to that applet's constructor, indicating a newly created LifeCycle object, each time you enter the applet's Webpage.

Listing 5. LifeCycle.java

 

// LifeCycle.java

public class LifeCycle extends java.applet.Applet { public LifeCycle () { System.out.println ("constructor called"); }

public void init () { System.out.println ("init() called"); }

public void start () { System.out.println ("start() called"); }

public void stop () { System.out.println ("stop() called"); }

public void destroy () { System.out.println ("destroy() called"); } }

Conclusion

Even after writing all of this material, I've only scratched the surface. There is so much more material that I could have covered. Perhaps I will write another sequel in the future. Or maybe you will write that sequel. Either way, let's continue to expand our understanding of Sun's Java Plug-in technology.

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more