SimpleCenter Hacking

From Omnifi Wiki

Jump to: navigation, search

Contents

SimpleCenter Issues

June 1, 2005 - Lumkichi

While SimpleCenter is pretty good, it does have its rough edges. The lastest update provided by OmnifiMedia has the following issues:

a)Unable to properly stream NullSoft ShoutCast
b)Unable to properly stream Yahoo LaunchCast
c)Unable to properly stream VirginRadio
d)Unable to catalogue more than 10,000 songs
e)If a song is deleted or moved OUTSIDE of SimpleCenter, then orphan links are created, and will not play
f)If a SimpleDevices.com site is down, then the Media Guide page shows "An Internet Connection Is Required."
g)Some songs, particularly VBR files give the wrong song duration which is carried over to the DMP1 and DMS1.

Additionally, the following issues are not so apparent:

h)Multiple instances of LaunchCast invoked for each request to stream LaunchCast
i)Multiple instances of ShoutCast invoked for each request to stream ShoutCast
j)Many error messages showing up in simplecenter.log, particularly when streaming
k)Duplicate media link can be created, especially when ripping to folders that are watched by SimpleCenter


These issues, when presented to OmnifiMedia, were met with a brazen "Noted," with overtones of "we're no longer supporting this software and hardware--Good Luck."

As the Yahoo! Omnifi Forum was born (created by amorsell), several coders attempted to address some of these deficiencies. This was possible because SimpleCenter is a Java application, built to run on Windows machines using JNI. It is also possible to port this application to Linux and even Mac OSX because of that.


Disclaimer

I am happy to impart my knowledge of SimpleCenter Reverse-Engineering to the group. This is not intended to be 3L33T or |-|4X0R. In fact, my intentions are far from it. Please do not modify anything in SimpleCenter for your financial gain (whether implied or unintentional). Some people have suggested I setup a PayPal account to accept donations for my work thus far. I am staying away from this idea (although I relish it greatly) because it might be construed as financial gain.

Also, for legal reasons, please do not modify SimpleCenter to work with other devices for which it was not intended. For example: do not modify SimpleCenter to work with NetGear's MP101's unique features. OmnifiMedia will well be within their rights to serve you right over to your local judiciary representative.

Rather, the following tutorial is presented to you for educational purposes under the fair use act and for helping to maintain a piece of software for the equipment we so dearly try to love (you know, the DMP1 & DMS1 by SimpleDevices). Any hint of wrong-doing or pending legal action and I will yank this tutorial immediately.

My intended audience is not the curious or a casual dabbler, but a solid Java Programmer. I am going to assume you know OOP and the Java Language. I don't expect you to know JNI or Log4J or XML, although knowledge of those things help a lot since SimpleCenter uses them extensively.

My method is also extremely archaic. There is no known term for it, so I'd like to coin one: "Compile In Place" If there is a better way, I'd like to know. But given that I have no access to the original source code and I can't simply press a button and build the project, I don't have much of a choice. It may not even be done the way topcoder99 and tcrown007 had done it when they made their changes.

On the flip side, my method works and works well. If you're careful with not clobbering versions of code and are comfortable with source and class files existing in the same folders then you'll have no problem with the rest of the tutorial.

~lumkichi

Decompiling is a Labor of Love

Much like Linux people love their Linux boxes even though it was a bear to set up, "and gosh, a new reiserfs. I need to recompile the kernel with reiserfs support..." Mac users love their Macs, Olds Cutlass owners love their Olds Cutlasses even though it leaks and burns oil.

You will find out that decompiling is a mixture of art and science and good, solid common sense. The working source code you decompile successfully become a treasure to you. But you must share it with others, not for piracy's sake, but for love of the art. If omnifi and simpledevices aren't going to keep this software up, someone's got to do it. If someone's already done the hardwork, post it so that others can benefit from it and do rapid development of their own fixes.

This Tutorial

It's 95% dedicated to the topic of decompiling. The knowledge gained here could be applied to other Java programs as well. But I am still sticking to the topic of Reverse-Engineering SimpleCenter. All of my examples will come from class files found in simplecenter.jar.

A small section called Determining What To Decompile And Fix exists to give you a feel for finding the right class file to decompile. I cannot teach you exactly what classes to decompile--that is a matter of cunning, common sense, style and motivation of the programmer. I will tell you what tools you can use to enable you to make your own decisions.

You don't need to decompile the entire project, only the ones you need to be concerned with. This is a big time-saver.

The first step is to ready an environment for yourself to accomplish this.

Setting Up To Reverse-Engineer SimpleCenter

Tools

You will need the following tools to get started:

WinZip available from WinZip.com or any of the popular download sites.

WinRar available from RarLab.com or any of the popular download sites.

A good Text Editor.  I use UltraEdit-32 9.00a available from www.ultraedit.com/
They are up to v 11.10a.

A Java Software Development Kit (Java SDK). I recommend version Sun's J2SE 1.4.2_04-b05
You can download either the straight J2SE SDK or SDK with NetBeans IDE.

A Java Decompiler.  You will need several.  I use DJ Fast Decompiler and JODE. 
In a pinch, I've had to upload a class file to the web and use SourceAgain to decompile.

I won't go into the details of the installations. Please check each program's installation instruction (particularly Sun's Java J2SDK. It is rather involved--unless they've streamlined it recently). If you download or have Java 1.5 then you'll have to add a parameter in your compiler command to ensure that it is compatible with SimpleCenter's JRE:

 c:> javac -source 1.4 <java file>

Setup Your Environment

Unpacking your SimpleCenter

I am going to speak from a Windows XP perspective (although Windows 2000 and 95/98 would be similar):

  1. create a folder in your C:\ drive called C:\SimpleCenter
  2. copy the contents of C:\Program Files\SimpleCenter to C:\SimpleCenter
  3. in the C:\SimpleCenter directory, rename simplecenter.jar to simplecenter.zip
  4. open it using WinZip and extract the contents to C:\SimpleCenter
  5. clean up your C:\SimpleCenter\ directory so it now contains:
  6.  .\AUTO-INF\
     .\CLIENT-INF\
     .\com\
     .\deprecated\
     .\lib\
     .\META-INF\
     .\org\
     .\POET\
     .\SS-INF\
     build.properties
     simplecenter.jar
     lax.jar
     help.jar
    
  7. Important Verify your C:\SimpleCenter\lib\ directory contains the following:
  8.  activation.jar
     alloy.jar
     commons-httpclient.jar
     izmcomjni.jar
     jh.jar
     jniwrap.jar
     kxml2.jar
     log4j.jar
     mail.jar
     NetComponents.jar
     OmniFiClient.jar
     s7help.jar
     simpleware_upnp.jar
     simpleware_upnp_av.jar
     soap.jar
     winpack.jar
    

Create Your Environment Helper

  1. Create a "setup.bat" file in your C:\SimpleCenter\ directory and add the following lines
  2.  @echo off
     SET EN=C:\SimpleCenter\
     SET CA=%EN%lax.jar;
     SET CA=%CA%%EN%help.jar;
     SET CA=%CA%%EN%lib\activation.jar;
     SET CA=%CA%%EN%lib\alloy.jar;
     SET CA=%CA%%EN%lib\commons-httpclient.jar;
     SET CA=%CA%%EN%lib\izmcomjni.jar;
     SET CA=%CA%%EN%lib\kxml2.jar;
     SET CA=%CA%%EN%lib\jh.jar;
     SET CA=%CA%%EN%lib\jniwrap.jar;
     SET CA=%CA%%EN%lib\lcxml2.jar;
     SET CA=%CA%%EN%lib\lo4j.jar;
     SET CA=%CA%%EN%lib\mail.jar;
     SET CA=%CA%%EN%lib\NetComponents.jar;
     SET CA=%CA%%EN%lib\OmniFiClient.jar;
     SET CA=%CA%%EN%lib\s7help.jar;
     SET CA=%CA%%EN%lib\simpleware_upnp.jar;
     SET CA=%CA%%EN%lib\simpleware_upnp_av.jar;
     SET CA=%CA%%EN%lib\soap.jar;
     SET CA=%CA%%EN%lib\winpack.jar;
     
     SET CLASSPATH=%CLASSPATH%;%CA%
     @echo CLASSPATH is now setup for compiling SimpleCenter
    
  3. Open up a DOS Shell / DOS Prompt (Start -> Run -> cmd.exe) and type in the following (highlighted in blue):
  4.  C:\> cd \simplecenter
     
     C:\SimpleCenter> setup
     CLASSPATH is now setup for compiling SimpleCenter
     
     C:\SimpleCenter>
    

The message "CLASSPATH is now setup for compiling SimpleCenter" indicates that you've added all of the necessary jar files used by SimpleCenter to the CLASSPATH. Believe it or not, if you had a file to compile, you could do so right now, from that DOS prompt above.

Preparing to Compile Something

A word of caution: Decompiling is not for the faint of heart!

It is not a straightforward process of simply decompiling code, making changes and recompiling. If indeed it worked like that then I'd sleep a little easier. But it's close.

I would say that in my efforts to make changes to a class file for which I have no source code, decompiling and making a re-compilable version of the code that does the same thing as the original takes at least 55% of my time.

Read this article on Decompiling Java. While it's a little dated (Sep 2002), the concepts are understandable (but not sound--I'll explain why, later). This site has compared three different decompilers and pointed out the flaws each one of them has. Some of the decompilers produced error-free code sometimes, others produced non-java code, others produced code that will not compile, and the worst decompiler produced re-compilable code that was incorrect in logic, thus it did not do what it was originally supposed to do (the worst kind of error). Here is a Google Page listing a bunch of decompilers.

Once you decompile a file, expect to have to massage it significantly just to make it recompile. Then you'll have to inspect the logic to make sure that the recompiled code will do what the original class file did. You can only expect to divine that from extensive testing using the original code and firing up SimpleCenter, then replacing it with your recompiled code and seeing if any behaviour has changed. Remember, if you touch it, you own it.

Setup Your Desktop Environment

There's a lot of getting setup here. These are my methods; feel free to adapt it for your situation. You should have already installed the required components (J2SE, WinRar, etc.). It is not necessary, but go ahead and associate .jar files to open with WinRar. Do not use WinZip to open .jar files (even though I told you to use WinZip to unpack, earlier).

Open up the following:

  1. Windows Explorer, showing C:\SimpleCenter\
  2. A Second Windows Explorer, showing C:\Program Files\SimpleCenter\
  3. Make a copy of the original C:\Program Files\SimpleCenter\simplecenter.jar (make several)
  4. Open C:\Program Files\SimpleCenter\simplecenter.jar in WinRar (don't use WinZip).
  5. Open a text editor. Notepad will suffice, but you'll quickly tire of it.
  6. Open a Command Prompt / DOS Prompt
  7. In DOS Prompt type
  8.  C:\> cd \SimpleCenter
    
     C:\SimpleCenter> setup
     CLASSPATH is now setup for compiling SimpleCenter
     
     C:\SimpleCenter> _
    

Now you're ready for the next step...

Decompile a File

We'll start with a simple file. In the Windows Explorer that's sitting on C:\SimpleCenter\, navigate down the following folders: com\simpledevices\simplecenter\common\contentdirectory\. In it, you'll see the following files:

.\test\
AVFileUtils.class
ContainerDisplayer.class
ContainerListDisplayer.class
ContentDirectoryActionListener.class
ContentDirectorySplitPane$1.class
ContentDirectorySplitPane$DisplayerListener.class
ContentDirectorySplitPane.class
ContentDirectoryTreeModel.class
ContentDirectoryUIHelper.class
DeleteMediaAction$1.class
DeleteMediaAction.class
IconDescriptionCellRenderer.class
localization.properties
treemodel.properties

If you've installed DJ Java Decompiler, then these class files will have a square blue icon associated with them. If you double-click the first file AVFileUtils.class, it should open up in DJ Java Decompiler, replete with colorized keywords and everything. It will also create the following file in that folder in which it was opened:

AVFileUtils.jad

Go ahead and rename this .jad file to .java.

Editing the File

Close DJ Java Decompiler (I do not recommend you work in this software), and open up the AVFileUtils.java you've just created in your Text Editor (if you use UltraEdit, associate .java files with UltraEdit). You should be able to view the file and see the requisite Java code. Make no changes to this file yet.

Go ahead and open C:\Program Files\SimpleCenter\simplecenter.log in your editor. If you use NOTEPAD, you've got a small problem--simplecenter.log changes, constantly. Even as you have it open the contents are changing and those changes will not be seen real-time. You'll have to close it down and open simplecenter.log again. A good text editor will detect these changes and reload the logs automatically (or at least ask you if it's OK to do so).

Q: What does this file do, anyway?
Well, you can sort of divine that from the editing process, but it is one of the files responsible for reading and setting the ID3 tags on .mp3's and .wma's.

   public static void updateFileMetaData(String path, Property properties[])
       throws IOException
   {
       if(path.toLowerCase().endsWith(".mp3"))
           updateID3MetaData(path, properties);
       else
       if(path.toLowerCase().endsWith(".wma"))
           updateWMAMetaData(path, properties);
   }

Linux and Mac users, don't get your hopes up too much here. This class in turn calls another class file WMAReader or ID3v2Utils which further call native methods (via JNI) to read and update the ID3 tags.

This method is invoked whenever you edit track information from within SimpleCenter's Media Library, and it might even be called by the CD Ripper (not verified, but a logical assumption).

I've incorporated a call to this method from ShoutCastStreamer.class and ASFStreamer.class files to write the ID3 tag information while it's recording the streaming content. (This update is available in SimpleCenter_LUpdate.exe, btw)

Recompiling the File

What you have is a complete file. If you've installed the ORIGINAL simplecenter.jar (after the webupdate to version 2.0.3.0011) and unpacked it, then the AVFileUtils.class will be one of the few files that can simply be decompiled, renamed and recompiled without errors.

Go to the DOS prompt and type in:

C:\SimpleCenter> javac com\simpledevices\simplecenter\common\contentdirectory\AVFileUtils.java

C:\SimpleCenter> _

If you got no errors, then you should have a new AVFileUtils.class with today's date and time! Congratulations--sort of. We're not 100% sure that this recompiled code does exactly the same thing as the original, especially because we don't have access to the original source code with which to compare our file. But we'll take it at face value.

When you compile your code, make sure you always do it from C:\SimpleCenter\ and specify the full pathname to the file you're compiling. Otherwise your javac may compile some files and have trouble with others as it cannot find the associated helper files several directories up.

C:\SimpleCenter> javac com\simpledevices\media\shoutcast\ShoutCastStreamer.java

C:\SimpleCenter> _

Q: So what happened to the AVFileUtils.class that was there before?
A: it has been overwritten with the new AVFileUtils.class that you just recompiled. If, for whatever reason, you absolutely MUST restore the original AVFileUtils.class, then go to the C:\SimpleCenter\simplecenter.jar and manually pluck just this one file and put it back into the target directory.

This is the scary part. Sometimes my changes have just gone so far south that it doesn't do ANYTHING that I really wanted it to, and I forgot to keep a copy of the originally decompiled .java source file, then my only alternative is to go back to the original copy of the class file and decompile again.

Don't hold your breath.
Often times when you recompile something, you'll get messages such as:

C:\SimpleCenter>javac com\simpledevices\simplecenter\common\contentdirectory\AVFileUtils.java
com\simpledevices\simplecenter\common\contentdirectory\AVFileUtils.java:213: cannot resolve symbol
symbol  : variable exception
location: class com.simpledevices.simplecenter.common.contentdirectory.AVFileUtils
               throw exception;
                     ^
1 error

Then you'll have to go edit the file because the decompiler didn't produce immediately recompilable code. You'll find that if you address one error, it produces 50 errors on the next attempt to recompile. This is a normal process. It may be because the compiler got past the initial fatal error and encountered the next 50, or you may have caused it by deleting one too many } or ) or a missing ; or whatever. Your skillz as a Java programmer have to come out and shine at this point.

Testing Your Changes

Here's where it gets interesting. If you're decompiling / editing / recompiling on your machine, then the SimpleCenter installation you've got MUST BE a development installation. You should expect that you'll screw it up badly, even to the point it requires a re-install.

Your Production server must be located on another machine, and if that's not feasible, then at least in another directory. Keep one stable copy that will work no matter what you're doing, and another in which you can play.

Shutdown the SimpleCenter Server from the systray. You cannot test any changes while it is running.

One of the programs you've opened in the previous section (Setting up your Desktop Environment) was WinRar opened to C:\Progam Files\SimpleCenter\simplecenter.jar. WinRar has a folders view similar to Windows Explorer.

You should be looking at the following files in WinRar:

..
AUTO-INF\
CLIENT-INF\
com\
deprecated\
META-INF\
org\
POET\
SS-INF\
build.properties
soapservices.properties
soaptypes.properties

Navigate down this view to the same place you've decompiled your AVFileUtils.class on your hard drive. That is: com\simpledevices\simplecenter\common\contentdirectory\. You should see the same list of files you encountered in the previous section.

Here's the magic: drag the AVFileUtils.class (not .java) from your Windows Explorer and drop it into the WinRar window. You'll be presented with a Dialog Box to which you'll simply respond with an OK. When the smoke clears, you'll see your AVFileUtils.class, with today's date (instead of the original 5/7/2004) and filesize.

You can leave your WinRar open for now.

Fire up SimpleCenter again. If all is well, then it will start up as if nothing had happened. If you've missed a file or forgot to add an internal class or something, it will error out and tell you something is very, very wrong.

If you're lucky and everything started up, then go invoke this class file by going to the Media Library and selecting a song and edit its track information (Artist Name, Title, Track #, etc.). Then check the simplecenter.log for any clues. Check the track info and verify that it's changed correctly.

Wait, there's nothing in the logs about this file ever being called! Well, of course. This class never writes to the logs, and even if it did nothing notable ever happened to warrant a log output, probably.

Go Ahead, Make My Changes

You've still got the AVFileUtils.java open in your editor. You know this file compiles and runs without crapping out. It seems to do the requested operation like its supposed to.

That's great, but it's not interesting is it? This class, as mentioned before, does not produce an output to the log file. But let's MAKE IT do so.

This is the package and import info for this class (available in the decompiled source):

package com.simpledevices.simplecenter.common.contentdirectory;

import com.simpledevices.media.mp3.*;
import com.simpledevices.media.windowsmedia.WMAReader;
import com.simpledevices.upnp.av.*;
import java.io.*;
import java.net.*;
import java.util.*; 

public class AVFileUtils { ... }


Add the following import statement to the head of the file (after the package statement):

import com.simpledevices.util.logging.Log;

Then in the following method, add the following line, highlighted in green:

   public static void updateFileMetaData(String path, Property properties[])
   
       throws IOException
   {
   	Log.info(AVFileUtils.class, "Path = " + path);
       if(path.toLowerCase().endsWith(".mp3"))
           updateID3MetaData(path, properties);
       else
       if(path.toLowerCase().endsWith(".wma"))
           updateWMAMetaData(path, properties);
   }

Recompile this code, shutdown SimpleCenter, drop the class file into the jar file (in WinRar) and start SimpleCenter back up. Now edit a track once more and check the simplecenter.log and you should see something like:

[31 May 2005 21:50:03,686] AVFileUtils     Path = C:\My Music\Jethro Tull\Jethro Tull - Aqualung.mp3

If you see a line similar to the one above, then you're in the zone. If not, then verify that your track info has indeed changed, check that your AVFileUtils.class is indeed the one you recompiled a second time (check time stamp) and check that you delivered the AVFileUtils.class (not AVFileUtils.java) to the correct folder in WinRar, check that you've updated the correct simplecenter.jar, etc. You get the idea.

Get Out There And Do The Snoopy Dance

You ought to be proud of yourself at this point. Do the Snoopy Dance

Tips for Decompiling

Decompilers are not infallible. Many of the free ones have been written in 1999. Even the renouned jad.exe has not been updated since 2002--that is, before Java2. Many decompilers are a front-end to jad (such as DJ Decomplier & Cavaj) so switching decompilers may still produce the same un-compilable code (right down to the same error message)..

Oddly enough (and luckily) different decompilers have trouble with mostly different things. One decompiler may not be able to properly reconstruct a particularly convoluted try / catch clause in one method, while another compiler screws up trying to decrypt a do / while loop (especially if it contains branching using break and continue).

As I will discuss later, you can use this concept of using multiple decompilers and patch together a single source file from clippits from various non-compilable sources. Again, the resultant file MUST BE VALIDATED through thorough testing to ensure it's doing what it was supposed to do.

Remember: just because you get a file to compile does not mean that it will do what it was originally supposed to do

Can't Stress Enough

This website says one of the prime benchmarks of a good decompiler is the ability to take a binary class file, decompile it, and have it recompile immediately and return a binary class file that is byte-for-byte same as the original class file.

This is counter-intuitive to taking an original source file, compiling it and having it decompile to the original source file. But it makes sense. What's compiled is a binary file that uses bytecode--the original constructs such as while (true) and for( ; ; ) are reduced to conditional branches and goto statements (yes, Java actually uses GOTOs -- it's just that WE cannot use GOTOs)

In my opinion, neither of these approaches are sound. Here's why. Consider the following stacktrace.

java.net.UnknownHostException: 192.168.1.15
 at java.net.PlainSocketImpl.connect(Unknown Source)
 at java.net.Socket.connect(Unknown Source)
 at java.net.Socket.connect(Unknown Source)
 at sun.net.NetworkClient.doConnect(Unknown Source)
 at sun.net.www.http.HttpClient.openServer(Unknown Source)
 at sun.net.www.http.HttpClient.openServer(Unknown Source)
 at sun.net.www.http.HttpClient.<init>(Unknown Source)
 at sun.net.www.http.HttpClient.<init>(Unknown Source)
 at sun.net.www.http.HttpClient.New(Unknown Source)
 at sun.net.www.http.HttpClient.New(Unknown Source)
 at sun.net.www.http.HttpClient.New(Unknown Source)
 at sun.net.www.protocol.http.HttpURLConnection.plainConnect(Unknown Source)
 at sun.net.www.protocol.http.HttpURLConnection.connect(Unknown Source)
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
 at java.net.URL.openStream(Unknown Source)
 at com.simpledevices.simplecenter.devices.DeviceRegistrar.downloadIcons(DeviceRegistrar.java:214)
 at com.simpledevices.simplecenter.devices.DeviceRegistrar.deviceFound(DeviceRegistrar.java:131)
 at com.simpledevices.synchronization.ExternalSynchronizationService.Alive(ExternalSynchronizationService.java:104)
 at com.simpledevices.upnp.av.extensions.SynchronizationServer.invokeAction(SynchronizationServer.java:18)
 at com.simpledevices.upnp.device.ControlServer.handleMessage(ControlServer.java:41)
 at com.simpledevices.upnp.http.HttpServer.handleMessage(HttpServer.java:121)
 at com.simpledevices.upnp.http.HttpServer.access$300(HttpServer.java:17)
 at com.simpledevices.upnp.http.HttpServer$HandlerThread.run(HttpServer.java:98)
 at java.lang.Thread.run(Unknown Source)

In the section highlighted in red, a method name is followed by a class name in parethesis, and a number. Any seasoned programmer would recognize this as a line number in the ORIGINAL SOURCE FILE. If I got a copy of DeviceRegistrar.java from which this binary was compiled, I can go to line 214 and spot the problematic statement that bubbled the error up.

If I add a line or two (or remove some comments), this statement at line 214 will most certainly change to line 215 or 216. If I recompile and run the code again till I get the same exception, the JVM will report that the error is on line 215 or 216. How does it know that? Surely, there's debugging info there, because the source isn't supplied with the simplecenter.jar file at all.

When I decompile this file, it's going to be devoid of any comments and prettiness. It might not even have the same structure as the original code ( my decompiler uses for ( ; ; ) instead of while (true), for example ). When I recompile, how can I ever expect to restore the original debugging information stored in the original binary file?

The claims above is saying that the decompiler should be able to reproduce the original file layout of the source java file. Otherwise it can't hope to create a re-compiled binary file that is byte-for-byte the same as the original.

Your resulting decompiled file may not be the same as the original. Heck, it can't be. And because of that, when it's recompiled it's GOING to produce a different binary file EVEN THOUGH IT DOES EXACTLY THE SAME THING. (I'll retract this statement later, if I prove myself otherwise). You shouldn't expect to have the same file output either.

You will ONLY know that you've gotten the correct decompiled file by

 a) inspecting the logic and applying a copious amount of common sense
 b) thorough testing (and I mean thorough) of the recompiled binary to ensure it does the same thing. It may not even do it the same way, but AS LONG AS the GOES INTO (before) = GOES INTO (after) and GOES OUTTA (before) = GOES OUTTA (after) then we should be OK.

Preaching Anarchy, am I? Perhaps...

Decompiling a Troublesome File

One of my first encounters with a troublesome file was com.simpledevices.simplecenter.guide.Guide.class. I needed to alter this file for the locally cached start page mod introduced in SimpleCenter I Update.

When I decompiled with DJ (the only decompiler I had at the time) I got something like this:

// Decompiled by DJ v3.7.7.81 Copyright 2004 Atanas Neshkov  Date: 6/1/2005 10:30:37 PM
// Home Page : http://members.fortunecity.com/neshkov/dj.html  - Check often for new version!
// Decompiler options: packimports(3) 
// Source File Name:   Guide.java

package com.simpledevices.simplecenter.guide;

import com.simpledevices.simplecenter.*;
import com.simpledevices.simplecenter.common.WebBrowserCommandHandler;
import com.simpledevices.simplecenter.common.WebBrowserPanel;
import com.simpledevices.simplecenter.omnifi.AccountManager;
import com.simpledevices.ui.GeneralAction;
import com.simpledevices.ui.VisibleComponentUpdater;
import com.simpledevices.upnp.av.AVItem;
import com.simpledevices.upnp.av.AVProperties;
import com.simpledevices.util.logging.Log;
import com.simpledevices.util.net.InternetUtils;
import java.awt.event.ActionEvent;
import java.io.UnsupportedEncodingException;
import java.net.*;
import javax.swing.*;

public class Guide
    implements Application, WebBrowserCommandHandler
{
    private class CheckConnectionAction extends GeneralAction
    {

        public void actionPerformed(ActionEvent e)
        {
            checkConnection();
        }

        public CheckConnectionAction()
        {
            super(Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction != null ? 
            Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction : 
            (Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction = 
            Guide._mthclass$("com.simpledevices.simplecenter.guide.Guide$CheckConnectionAction")));
        }
    }


    public Guide()
    {
        appInfo = new ApplicationInfo(com.simpledevices.simplecenter.guide.Guide.class);
    }

    protected Guide(ApplicationInfo info)
    {
        appInfo = info;
    }

    public void init(ApplicationContext context)
    {
        accountManager = (AccountManager)context.get("AccountManager");
    }

    public JComponent getPanel(JFrame frame)
    {
        if(appPanel == null)
        {
            appPanel = new ApplicationPanel(appInfo.getName(), null);
            browser = new WebBrowserPanel();
            String guideUrl = getGuideUrl();
            browser.setHome(guideUrl);
            if(initialUrl != null)
                browser.navigate(initialUrl);
            else
                browser.navigate(guideUrl);
            WebBrowserPanel _tmp = browser;
            WebBrowserPanel.addCommandHandler(this);
            initialUrl = null;
            noConnectionPanel = new JPanel();
            noConnectionPanel.add(new JLabel("An internet connection is required."));
            noConnectionPanel.add(new JButton(new CheckConnectionAction()));
            checkConnection();
            appPanel.addAncestorListener(new VisibleComponentUpdater(5000L) {

                protected void update()
                {
                    checkConnection();
                }

            });
        }
        return appPanel;
    }

    public ApplicationInfo getInfo()
    {
        return appInfo;
    }

    private String getGuideUrl()
    {
        String baseUrl = getBaseUrl();
        String accountId = accountManager.getAccountId();
        if(accountId != null)
            baseUrl = baseUrl + "?accountId=" + accountId + "&version=" + System.getProperty("build");
        return baseUrl;
    }

    protected String getBaseUrl()
    {
        return System.getProperty("com.rockford.omnifi.guide.url");
    }

    private void checkConnection()
    {
        try
        {
            URL root = new URL(System.getProperty("simpleserve.sms.rooturl"));
            if(!InternetUtils.isConnectionAvailable(root.getHost(), root.getPort()))
            {
                if(browser.getParent() != null || noConnectionPanel.getParent() == null)
                {
                    appPanel.remove(browser);
                    appPanel.add(noConnectionPanel, "Center");
                    appPanel.validate();
                }
            } else
            if(noConnectionPanel.getParent() != null || browser.getParent() == null)
            {
                appPanel.add(browser, "Center");
                appPanel.remove(noConnectionPanel);
                appPanel.validate();
            }
        }
        catch(MalformedURLException e)
        {
            Log.error(com.simpledevices.simplecenter.guide.Guide.class, e);
        }
    }

    public void removeApplication()
    {
    }

    public void initApplication()
    {
    }

    public Object message(String messageId, Object parameters[])
    {
        Object result = null;
        try
        {
            if(messageId.equals("MusicExplorer"))
            {
                StringBuffer url = null;
                if(parameters.length > 0)
                {
                    url = new StringBuffer(System.getProperty("simpleserve.sms.rooturl"));
                    url.append("/musicexplorer.sms");
                    AVItem item = (AVItem)parameters[0];
                    url.append("?lookupType=" + parameters[1]);
                    String artist = (String)item.getProperty(AVProperties.ARTIST_PROPERTY);
                    if(artist != null)
                        url.append("&artistName=" + URLEncoder.encode(artist, "UTF-8"));
                    String album = (String)item.getProperty(AVProperties.ALBUM_PROPERTY);
                    if(album != null)
                        url.append("&albumName=" + URLEncoder.encode(album, "UTF-8"));
                    String title = item.getTitle();
                    if(title != null)
                        url.append("&trackName=" + URLEncoder.encode(title, "UTF-8"));
                    Log.info(com.simpledevices.simplecenter.guide.Guide.class, "Search url: " + url);
                    if(browser == null)
                        initialUrl = url.toString();
                    else
                        browser.navigate(url.toString());
                }
            }
        }
        catch(UnsupportedEncodingException e)
        {
            Log.error(com.simpledevices.simplecenter.guide.Guide.class, e);
        }
        return result;
    }

    private void resetGuidePanel()
    {
        if(browser != null)
        {
            browser.setHome(getGuideUrl());
            browser.navigate(getGuideUrl());
        }
    }

    public void handleCommand(String commandString)
    {
        Log.info(com.simpledevices.simplecenter.guide.Guide.class, "Received commandString: " + commandString);
        int parameterIndex = commandString.indexOf('(');
        String command = commandString;
        if(parameterIndex != -1)
            command = commandString.substring(0, parameterIndex);
        if(command.equals("updateaccount"))
        {
            if(parameterIndex == -1)
                Log.error(com.simpledevices.simplecenter.guide.Guide.class, "Incorrect parameters");
            String accountId = commandString.substring(parameterIndex + 1, commandString.length() - 1);
            if(accountId != null)
            {
                accountManager.setAccount(accountId);
                resetGuidePanel();
            }
        }
    }

    public static final String MUSIC_EXPLORER_MESSAGE = "MusicExplorer";
    private ApplicationInfo appInfo;
    private ApplicationPanel appPanel;
    private JPanel noConnectionPanel;
    private WebBrowserPanel browser;
    private String initialUrl;
    private AccountManager accountManager;
    private static final String UPDATE_ACCOUNT_COMMAND = "updateaccount";
    static Class class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction; /* synthetic field */

}

Looks great! I can read it and tell a lot about what this class does. There's a small problem:

C:\SimpleCenter> javac com\simpledevices\simplecenter\guide\Guide.java
com\simpledevices\simplecenter\guide\Guide.java:36: cannot resolve symbol
symbol  : method _mthclass$ (java.lang.String)
location: class com.simpledevices.simplecenter.guide.Guide
perchance you meant '_mthclass.'
            super(Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckCo
nnectionAction != null ? Guide.class$com$simpledevices$simplecenter$guide$Guide$
CheckConnectionAction : (Guide.class$com$simpledevices$simplecenter$guide$Guide$
CheckConnectionAction = Guide._mthclass$("com.simpledevices.simplecenter.guide.G
uide$CheckConnectionAction")));
                             ^
1 error

C:\SimpleCenter> _

The darn thing won't compile. The error message does suggest very politely what the problem might be. ("perchance you meant '_mthclass.'") Yes, perchance I did. I change line 36 from ._mthclass$ to ._mthclass. like it suggests and try again.

C:\SimpleCenter> javac com\simpledevices\simplecenter\guide\Guide.java
com\simpledevices\simplecenter\guide\Guide.java:36: <identifier> expected
            super(Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckCo
nnectionAction != null ? Guide.class$com$simpledevices$simplecenter$guide$Guide$
CheckConnectionAction : (Guide.class$com$simpledevices$simplecenter$guide$Guide$
CheckConnectionAction = Guide._mthclass.("com.simpledevices.simplecenter.guide.G
uide$CheckConnectionAction")));
                                        ^
com\simpledevices\simplecenter\guide\Guide.java:36: ')' expected
            super(Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckCo
nnectionAction != null ? Guide.class$com$simpledevices$simplecenter$guide$Guide$
CheckConnectionAction : (Guide.class$com$simpledevices$simplecenter$guide$Guide$
CheckConnectionAction = Guide._mthclass.("com.simpledevices.simplecenter.guide.G
uide$CheckConnectionAction")));
                              ^
2 errors

C:\SimpleCenter> _

Gosh, I went from bad to worse!!!!

Clean it up

Long story short, I futzed with it all week, until I asked the forum for a re-compilable version of guide.class. topcoder99 obliged. His changes were simple

He made 2 small changes. Changed line 36 from:

            super(Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction !=
            null ? Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction : 
            (Guide.class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction = 
            Guide._mthclass$("com.simpledevices.simplecenter.guide.Guide$CheckConnectionAction")));

to:

            super( Guide.class );

and commented out the very last line where it described a "synthetic field."

// static Class class$com$simpledevices$simplecenter$guide$Guide$CheckConnectionAction; /* synthetic field */

CheckConnectionAction.class is the name of the named inner class that needs to register itself to its parent class ( GeneralAction.class ). This super() method appeared inside the constructor of this inner class. The class will still compile if I change it to:

            super( CheckConnectionAction.class );

I'm not exactly sure what the difference is: it seems to work either way. Well at least, DJ ( jad.exe, really ) can decompile the inner classes (something that mocha decompiler can't do very well).

Now this thing compiled, and I could plug it back into the simplecenter.jar to test it out. It worked reasonably well.

From this point forward, I was able to make the necessary changes and enable the Locally Cached Page mod. It's not great, but it was something that gave me ideas.

Other Challenging Problems

In the C:\SimpleCenter\com\simpledevices\simplecenter\audio\medialibrary folder, there is a class file called MediaLibraryController.class.

Actually, there are 24 files! 23 of them are inner classes of MediaLibraryController.class. This is a more challenging one to tackle because that strange string found in line 36 of Guide.java appears a bah-jillion times in this one! Sometimes twice or three times on a single line.

MessageDialog.showMessageDialog(frame, 
MediaLibraryController.class$com$simpledevices$simplecenter$audio$medialibrary$MediaLibraryController
!= null ?  MediaLibraryController.class$com$simpledevices$simplecenter$audio$medialibrary$MediaLibraryController
:  (MediaLibraryController.class$com$simpledevices$simplecenter$audio$medialibrary$MediaLibraryController
=  MediaLibraryController._mthclass$("com.simpledevices.simplecenter.audio.medialibrary.MediaLibraryController")),
"DeleteFromPlaylistFail.title", "DeleteFromPlaylistFail.message", 0);

Needless to say, I had to a lot more to contend with. I patiently spent a couple of days carefully reducing these statements back down to something simple.

Additionally, there were several places where the try / catch blocks did not properly decompile.

   public String getCurrentPlaylistGroup()
   {
       String s = null;
       AVObject avobject;
       Object obj = null;
       if(tree != null)
       {
           TreePath treepath = tree.getSelectionPath();
           if(treepath == null && treepath.getPathCount() >= 2)
           {
               AVObject avobject1 = (AVObject)treepath.getPathComponent(1);
               if(avobject1.getCreator().equals("USER"))
                   s = avobject1.getId();
           }
       }
       if(s != null)
           break MISSING_BLOCK_LABEL_188;
       AVObject aavobject[] = contentDirectory.browse("0", "BrowseDirectChildren", "*", 0, 0, null).getResult();
       int i = 0;
       if(i >= aavobject.length)
           break MISSING_BLOCK_LABEL_188;
       avobject = aavobject[i];
       if(!avobject.getCreator().equals("USER"))
           return avobject.getId();
       break MISSING_BLOCK_LABEL_188;
       Object obj1;
       obj1;
       Log.error(com.simpledevices.simplecenter.audio.medialibrary.MediaLibraryController.class, ((Throwable) (obj1)));
       break MISSING_BLOCK_LABEL_188;
       obj1;
       Log.error(com.simpledevices.simplecenter.audio.medialibrary.MediaLibraryController.class, ((Throwable) (obj1)));
       return null;
   }

Get Creative

To address this, I've resorted to using as many different types of decompilers against this piece of code. While no one decompiler produced a complete, re-compilable file, I did manage to come across one that decompiled just this one method. I cleaned it up some:

   public String getCurrentPlaylistGroup()
   // decompiled using jreversepro
   {
        AVObject[] avobjectArr;
        String string=null;
        int i;
        try {
             Object object = null;
             AVObject object4;
             if (tree != null) {
                  TreePath treepath = tree.getSelectionPath();
                  if (treepath == null && treepath.getPathCount() >= 2) {
                       AVObject avobject = (AVObject)treepath.getPathComponent(1);
                       if (avobject.getCreator().equals("USER")) 
                            string = avobject.getId();
                       
                  }
             }
             if (string == null) {
                  avobjectArr = contentDirectory.browse("0" , "BrowseDirectChildren" , "*" , 0 , 0 , null).getResult();
                  i = 0;
                  for (;i < avobjectArr.length;) {
                       object4 = avobjectArr[i];
                       if (object4.getCreator().equals("USER")) 
                            break;
                       
                       return (object4.getId());
                  }
             }
        } catch (UPnPServiceException upnpse) {
             Log.error(MediaLibraryController.class, upnpse);
        } catch (IOException ioe) {
             Log.error(MediaLibraryController.class, ioe);
       }
        
        return null;
   }

Be prepared to get creative. Get a copy of JReversePro, JODE, DJ, and any commercial decompilers you can get your hands on.

Still, I did not know if this massive class file would behave properly. My first attempt back in late April produced a compiled file that caused many, many problems when executed, particularly with Playlists and stuff. It was for experimentation anyway (I was trying to find the exact method that SimpleCenter used to delete a media item from its database).

But times necessitated me to get it right so that I could put a button on the toolbar for invoking the Orphan Checker pop up box.

If in a Pinch

If you google for java decompilers, you'll get listings of the standard, free ones: Jad, DJ, Cavaj, JCavaj, JODE, JReversePro, Mocha-b1. You'll also notice that, all of them are old--more than two and in some cases 5 or more! Mocha is only Java 1.1 compatible.

Development of open-source Java decompilers seems to have stopped 3 years ago, and maintenance/upkeep of existing line of code isn't keeping up either (Mocha's developer died 5 years ago, JAD's writer is so confident of his current code, he doesn't bother). To top it off, Java itself is changing (Java 1.5 is GA, and Java 1.6 around the corner). There are a great many papers on the theory and practice of decompiling from bytecode--it's just difficult to keep up. Commercial companies seem to be the only ones with any development dollars for this sort of thing.

When decompiling ASFStreamer.class (used by LaunchCast that is streamed to the DMS1), I found that I could not effectively decompile this sucker, no matter which version of free decompilers I used. JODE would crap out with an Exception, JAD, MOCHA, JReversePro all fell on its butt. I couldn't tell with any degree of certainty how topcoder99's changes were made.

I had a couple options, still. One was to purchase a nice Commercial one for about $99. I doubt if I could scrape together that much money myself (doh--I'm a cheapskate, actually) and I did not want to ask the group for a donation to buy me a copy. The other was to guess at the broken logic and see if I could piece together a workable version. Or, I could try to contact topcoder99 and ASK for his copy of the source file.

I did the next best thing: a combination.

I discovered that I could decompile the ORIGINAL (pre-topcoder99's) ASFStreamer.class. This was extremely helpful. But this piece of code did not properly stream LaunchCast. Topcoder99 made three changes to this file: one to fix the streaming problem, one to enable playback of 64kbps LaunchCast, and another to allow it to record the stream to disk.

While I could guess at where and how to record to disk and make topcoder's changes retroactively, I did not have a clue as to how he repaired the streaming problem or what was added to stream 64kpbs. I never knew what the original problem was... :(

I went to SourceAgain's website to see if I could download a trial copy. To my surprise, they had a web-version that could decompile class files (Applets) stored on the Internet, for free.

I uploaded the ASFStreamer.class and ASFStreamer$HeaderInfo.class files up to my personal web page and pointed the web-decompiler to the URL and agreed to its terms. --Whee--

Now I had enough pieces to reconstruct a workable file. SourceAgain reported that there was a corrupt reference to a goto that could not be resolved, but it printed out something that I could plug back into my .java file and ponder

Leap of Faith

Sometimes you have to do what you have to do. No other way around it...

I've decompiled as far as the decompilers can take it. There are still errors. I look at the output of one Decompiler which uses a while(true) construct and another that uses a for( ; ; ) construct to set up a perpetually-repeating loop. The innards look similar, but aren't the same. But one does look more apropos than the other.

I close my eyes, pick one, and feverishly compare the blocks of troublesome code and hash out what I believe should happen. I rearrange the weird try / catch blocks, so that the errors it catches are caught inside the block. I close my eyes when I see strange pieces of code such as:

           try
           {
label_0:
               try
               {
                   break label_0;
               }
               finally
               {
                   if( asfconnection1 != null )
                       asfconnection1.close();
               }
           }
           catch( ASFFormatException asfformatexception2 )
           {
               Log.error( (class$com$simpledevices$media$windowsmedia$ASFStreamer == null) ? 
                  class$com$simpledevices$media$windowsmedia$ASFStreamer = class$( 
                  "com.simpledevices.media.windowsmedia.ASFStreamer" ) : 
                  class$com$simpledevices$media$windowsmedia$ASFStreamer, 
                  (Throwable) asfformatexception2 );
               throw new IOException( asfformatexception2.getMessage() );
           }
           return;

and pray that it'll compile. Surprisingly it does, after I clean up that and rearrange the try/catch blocks. Whew!!!!

                      if( asfconnection1 != null ) {
                           try {
                               asfconnection1.close();
                           } catch( IOException ioexception1 ) {
                           }
                       }
                       
                   }
               } catch( ASFFormatException asfformatexception1 ) {
                   Log.debug( ASFStreamer.class, asfformatexception1.toString() );
                   throw new IOException( asfformatexception1.getMessage() );
               } finally {
                   if (asfconnection1 != null ) {
                       
                       asfconnection1.close();
                   }
               }
               return;

I stared at this code long an hard. And you may ask why am I catching an IOException where it was catching an ASFFormatException? Simple: the compiler told me that closing asfconnection1 did not throw an ASFFormatException.

The point I'm trying to make is that sometimes all feasible (as opposed to all available) avenues will fail to help you. No systematic approach is going to get you out of a bind and you can't contact the author.

Your only option is to carefully inspect the code, determine what the intent of it is, and reconstruct it to the best of your ability. You might break a thing or two doing so--that's what testing is for. Put in a lot of debug statements and then follow the logic around until you've repaired that poor ol' java file to a usable condition.

Determining What To Decompile And Fix

This is a difficult section to describe. It should be the most in-depth and detailed. Instead, it is the smallest section with much of it left as an exercise for the reader and future program-fixer.

There is no correct answer in approaching what to repair and how. You will only know what to do if you tinker and prod and probe, even when nothing is wrong. Consider doing the following from time-to-time:

a) Drill down the various directories, looking at the class names--seeing what might be where and why.
b) Decompile a couple of classes just to see what's in there. Try some of the "controller" classes such as MediaLibraryController.class
c) See if you can recompile a class or two, just to see what kinds of errors you get that you'll have to overcome. In doing so, you'll begin to learn what the particular class or method you're fixing is supposed to do.
d) See if you can get it to write out to the logs and watch what's in there and when it reacts.
e) Look at the logs for Exceptions. See which classes they're being thrown by and attempt to decompile that class. Drill down to see where the original source of the error was.

Reasons for Decompiling

I can think of three:

1) To repair a problem that's occurring (Exceptions or incorrect behaviour)

Usually, you're going in to repair a problem with SimpleCenter. You should have a pretty good idea what module it is. See if you can duplicate the problem yourself (reliably) and document those steps so you can describe it to another person. Scan the logs for any evidence of those problems and see which Class file is throwing those errors or writing the strange outputs. From there, you'll begin to decompile a class or two (without regard to having to recompile it) to see what other methods and classes it calls. Eventually you'll strike the heart of the problem and be able to follow the logic enough to repair it. Now you'll have to create a re-compilable source file so you can test your fixes.

2) To add new functionality where none existed before

This is harder. While you may have an idea how to go about doing something, it's very difficult to be able to pick exactly where to start modifying.

For example, the Orphan Checker was new functionality built into SimpleCenter. I spent weeks, literally, trying to decide where to put this code. I started in the MediaLibraryController.class trying to see if I could tap into the DefaultDeleteMedia action to delete any media my code would find. It got to be too cumbersome, and I gave it up.

After some time wandering, peeking and poking (programmers know what this means), I came across the SimpleCenterModel.class which gets called when SimpleCenter is started up. It does a lot of things, and it's readily decompilable (thank goodness) and best of all, it had all of the references that I needed. Following method calls in SimpleCenterModel, I was able to determine how a media was deleted, and I could copy that code and paste it into my OrphanChecker.class and call it from SimpleCenterModel as a background thread. Ingenious!

You have to get lucky on this type of change.

3) To tinker

I do this a lot. You ought to do this a lot too; it's the best way to learn.

Use the Logs to Your Advantage

Pepper your class files with Log.info() messages and see where the execution is branching off to next. When you're satisfied that you've arrived, restore the original class files without the Log outputs so that you don't fill it up with no longer necessary entries.

I don't think I could do a thing without the logs. Even when someone on the forums says they have a problem, I ask for their logs. Good logs say volumes about their computer and their apparent issue.

Using your Super-Wamperdyne Text Editor

I have a function in UltraEdit called Find and Replace. Most text editors have this. I also have two other functions called Find in Files and (gasp) Replace in Files. These are more powerful...

They can find text strings inside multiple files and report how many times they occured in the files. This is similar to the *nix command:

# find . -type f -exec grep "searchtext" {} \;

I use this command line a lot, by the way. But the nice thing in UltraEdit is that I can right-click any of the results and select "Open file ..." and voila! The Replace in Files is very dangerous and I won't cover its use here--it's only destructive so stay away.

The reason that Find in Files is so useful is that suppose you've just decompiled a class file (let's say the AVFileUtils.class we've covered above). You want to know where the method updateFileMetaData() is referenced, but you don't want to decompile each class file just to look inside the source. The solution is simple: simply start at the C:\SimpleCenter\com folder and Find in Files for that method name (minus the parentheses). Let UltraEdit look down the subdirectories and match everything it can (*.*).

The reason is simple: except for local variables, most (if not all) variable names and method names remain visible as-is so that the Java Reflection can look inside the file and match a method name against your code you're trying to compile. You can simply do a text search for that method name in any binary class file.

When you get certain results, you can go to the directory and double click them to decompile those classes using JAD and open them up in the text editor for a closer inspection. Nifty.

Other tricks, hmmm. Suppose you want to know how editing the media tag info works. You right click a track and get the "Edit track" option. When you open that window, you notice that it has a title: "Edit Media Information...". You can perform a Find in Files for this string, and it should return

Find 'edit media information' in 'C:\SimpleCenter\com\simpledevices\simplecenter\audio\localization.properties' :
C:\SimpleCenter\com\simpledevices\simplecenter\audio\localization.properties(37): EditTracksName=Edit Media Information...
C:\SimpleCenter\com\simpledevices\simplecenter\audio\localization.properties(52): EditTrackInvalidDataHeader=Edit Media Information Error
Found 'edit media information' 2 time(s).

Right-clicking one of these lines should open up the appropriate localization.properties file. Just go to the line number shown in the parentheses to find your string. But wait--how come no class files came up in the search? A properties file doesn't run the code and stick the title up in the window...

True. What the SimpleCenter program is designed to do is to allow for localization. (In Visual Basic and Visual C++ it's called a Resource Bundle). By having multiple copies of the same localization.properties in different languages, one could conceivably make SimpleCenter talk French, Dutch or even Chinese, by simply swapping out these text files. The beauty is that the code does not have to be recompiled. The disadvantage is that it makes your debugging one step harder.

In the results, you'll see that the string "Edit Media Information..." is a value to the variable name "EditTracksName". If now you Find in Files for the string "EditTracksName"

Find 'EditTracksName' in 'C:\SimpleCenter\com\simpledevices\simplecenter\audio\EditTracksAction.class' :
C:\SimpleCenter\com\simpledevices\simplecenter\audio\EditTracksAction.class(67): SourceFile
Found 'EditTracksName' 1 time(s).
------------------------------
Find 'EditTracksName' in 'C:\SimpleCenter\com\simpledevices\simplecenter\audio\localization.properties' :
C:\SimpleCenter\com\simpledevices\simplecenter\audio\localization.properties(37): EditTracksName=Edit Media Information...
Found 'EditTracksName' 1 time(s).
------------------------------
Search complete, found 'EditTracksName' 2 time(s).

Now we're getting somewhere. Go ahead, decompile the classfile EditTracksAction and you'll see that there is a line

putValue("Name", Localization.getString(EditTracksAction.class, "EditTracksName"));

And you can see what this class does (incidentally this is the class that builds the JPanel of all of the buttons and text widgets as well as performs the data validation and input error handling for the task of editing media tag info).

See what I mean? Pretty soon, you'll be following bits of code from one class to another to a properties file to, well pretty much everything.

Dump the System Properties

SimpleCenter stores a number of "global" values in the System Properties repository. It also stores a few things in the Preferences repository. It would behoove you to dump the contents of the System Properties and the Preferences so you can see what you have (or don't have) access to.

In one of the recompiled code (like AVFileUtils), you might stick the following code in (just for debugging purposes: never allow this code to go to production):

StringBuffer text = new StringBuffer();
Properties props = System.getProperties();
Enumeration e = props.propertyNames();
while (e.hasMoreElements())
   {
   key = (String) e.nextElement();
   value = props.getProperty (key);
   text.append("Key :" + key + ": = :" + value + ":\n");
   }
Log.info( AVFileUtils.class, text.toString() );

Run the code, and take actions to invoke your bit of injected code, and quickly go inspect simplecenter.log for the results. Save this information off so you have access to it later. A good programmer would have bits and pieces of these notes. Better yet, a notebook with this info would be even better.

Develop a similar piece of code for enumerating (or iterating, if that's your thing) the Preferences and see what you get as well... Make sure you do this from inside SimpleCenter so you can capture what SimpleCenter writes or reads from the System Properties.

IMPORTANT: Always remove the unnecessarily injected code from your source files, or at least move them off to a place where they will not compile. Restore the original class file to ensure that your modified code does not stay in the jar file.

Tips For Debugging and Logging

Seasoned programmers use a lot of System.out.println() statements throughout the code to write out statuses as the program runs. Messages such as "I am HERE" and "value = 15, filename=C:\My Music\jump.mp3" are not uncommon.

Try not to use System.out.println statements in your code. There are a number of reasons.

1) There is no stdout console any longer. Your System.out.println()'s get written to stdout. System.err.println()'s go to stderr. On your Windows Desktop, do you see anything that looks like a terminal with messages?

2) System.out.println() take a huge toll on performance. Java just doesn't print anything out to the screen fast.

Large Log Files

The simplecenter.log is set to a maximum size of 10 Mb, at which point it rolls. If you're using NOTEPAD to scan this file, it'll take a considerable amount of time to open the file.

Luckily, you can delete the file entirely, or cut away portions you're not interested in and save it. Unfortunately, you can only alter the log file or delete it when SimpleCenter is not running. Go ahead and shutdown SimpleCenter and delete the logs. Fire it back up, and watch a new file be created in the C:\Program Files\SimpleCenter\ directory.

Now you have a much more managable log file to open.

Log4J

The makers of SimpleCenter had the foresight to use Log4J as the logging mechanism. They've also made it very, very easy to enable logging in your code.

Import the proper package (highlighted in green):

import com.simpledevices.upnp.av.*;
import com.simpledevices.upnp.av.mediaserver.MediaStreamer;
import com.simpledevices.upnp.av.mediaserver.MetaDataChangedListener;
import com.simpledevices.simplecenter.common.contentdirectory.AVFileUtils;
import com.simpledevices.util.io.IOUtils;
import com.simpledevices.util.logging.Log;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import sun.net.www.protocol.http.Handler;

and simply use the static methods that Log provides throughout your code:

Log.info( <Object.Class> , <Java.lang.String> );
Log.info( <Object.Class> , <Java.lang.Exception> );
Log.warn( <Object.Class> , <Java.lang.String> );
Log.warn( <Object.Class> , <Java.lang.Exception> );
Log.error( <Object.Class> , <Java.lang.String> );
Log.error( <Object.Class> , <Java.lang.Exception> );
Log.debug( <Object.Class> , <Java.lang.String> );
Log.debug( <Object.Class> , <Java.lang.Exception> );

Here is a snippet out of ShoutCastStreamer.java that I've modified lately:

   try {
     if (myFileOutputStream != null) {
       myFileOutputStream.close();
       myFileOutputStream = null;
       if ( debug ) Log.info ( ShoutCastStreamer.class, "(" + iAm + ") " + "I've closed the file.");
       
       if (myPrefs.getBoolean( "RECORDING_NOW" , false)) {
         myPrefs.putBoolean( "RECORDING_NOW" , false );
       }
       
       if ( framesRecorded < minimumFrames ) {
         Log.warn( ShoutCastStreamer.class, "(" + iAm + ") " + "Less than 10 frames recorded on " + myFile.getName() + ". Deleting.");
         myFile.delete();
       } else {

       Property[] newProperty = new Property[2];
       newProperty[0] = new Property(AVProperties.ARTIST_PROPERTY, artist);
       newProperty[1] = new Property(AVProperties.TITLE_PROPERTY, title);
       if (debug) {
           Log.info( ShoutCastStreamer.class, "(" + iAm + ") " + "Updated ID3 tags. File is '" + thisFileName + "'" );
       }
       
       AVFileUtils.updateFileMetaData(thisFileName, newProperty);
       
       }
     }
   } catch (IOException ioe) {
   }
 

The messages would be logged in C:\Program Files\SimpleCenter\simplecenter.log. It would show the datetime stamp, the class name which invoked the logger and the text message associated with it.


Currently, Log4J's mask is set to record EVERYTHING that is sent to it. In many production environments which use Log4J, even logging is a performance issue to deal with and typically only set to record Errors and maybe Warnings. In SimpleCenter, there is not as much concern about highspeed performance.

Log4J takes the bother out of trying to set up your own logging mechanism. It also provides for automatic log rolling (that is, once a log reaches a certain size, it is closed, renamed and a new, empty file is started). The older logs you may have would be in C:\Program Files\SimpleCenter\simplecenter.log.1

Why Log4J?

This is a question that is not too difficult to answer. Log4J does provide great benefits.

a) it's really easy to use. Once it's setup (the setup is not difficult either), it only a matter of importing the Log4J logger and then use a static method to start logging.
b) it can be configured to roll logs and control the max size of a log file
c) it can filter by severity levels (debug, info, warn, error)
d) it can filter by class. If you specify a particular class, Log4J can be trained to ONLY log stuff logged by that class. This is done by the magic of specifying a class name when invoking Log4J each time.
e) surprisingly, these settings can often be changed On-The-Fly. Most loggers need to be shutdown, the properties changed, and application restarted. Log4J can be trained to look for changes every x-milliseconds...
f) the output log format is extremely flexible (and extensible, I think). The way it's implemented on SimpleCenter reports WHICH classfile logged the message, which makes it easy to debug and better, where to go about looking to see how to expand it's capabilities.
g) it's free and itself is community supported and widely accepted.

SimpleCenter is not configured to take advantage of c), d) and e) above but if you felt so inclined, you easily could.

Writing your own logger entails a lot of file management overhead. Additionnally you may be receiving messages from multiple threads and your logger needs to be able to handle each thread pumping out log info as fast as they can, and be able to log them in the proper order they were received. Your application may not be such "industrial-strength," but these are some concerns when writing your own logger... Why bother, if you can use Log4J instead?

TroubleShooting

Don't see your logging message in simplecenter.log?

So you add Log.info() throughout your code and drop it into the simplecenter.jar and fire up SimpleCenter. You can see your messages in simplecenter.log. But for some reason, you just don't see what you expect to see next. No error messages, no nothing.

The code you expected to execute next just doesn't seem to have run. What's going on?

Consider the following code out of ShoutCastStreamer.java (it's where I've encountered the most difficulties):

       if(metaData != null)
       s = metaData.getProperty(AVProperties.ARTIST_PROPERTY) + "-" + metaData.getProperty(AVProperties.TITLE_PROPERTY);
       if(!s.equals(s1) && s.length() > 1)
       {          
         s1 = s;
         artist = (String)metaData.getProperty(AVProperties.ARTIST_PROPERTY);
         title = (String)metaData.getProperty(AVProperties.TITLE_PROPERTY);
         Log.info( ShoutCastStreamer.class, "OK, I'm here.");
         if ((myFileOutputStream != null))
         {
           if ( debug ) Log.info(ShoutCastStreamer.class, "(" + iAm + ") " + "Closing File.");
           
           closemyFileOutputStream();
           
         }
         myFile = new File("stream");
         myFile.mkdir();
         myFile = new File("stream" + File.separatorChar + s + ".mp3");
  ...

For some reason, I am not seeing the statement "OK, I'm here" in the simplecenter.log to tell me that program execution has gotten to that point. Everything else leading up to it was OK, but the Streamer simply stops working and music on the DMS1 stops, but the logs show nothing... The code compiled fine.

For those who can see the fallacy in the above code (especially in the areas highlighted green) might recognize that there might be some sort of ERROR (not exception) that is not handled and the code execution jumps out of this class and back up to the container that called it. Eventually the error should bubble up to the HttpServer$ServerThread and it should dump a stack trace but sometimes it doesn't.

Oddly enough, when I modified the code to read:

       if(metaData != null)
       s = metaData.getProperty(AVProperties.ARTIST_PROPERTY) + "-" + metaData.getProperty(AVProperties.TITLE_PROPERTY);
       if(!s.equals(s1) && s.length() > 1)
       {          
         s1 = s;
         artist = "" + metaData.getProperty(AVProperties.ARTIST_PROPERTY);
         title = "" + metaData.getProperty(AVProperties.TITLE_PROPERTY);
         Log.info( ShoutCastStreamer.class, "OK, I'm here.");
         if ((myFileOutputStream != null))
         {
           if ( debug ) Log.info(ShoutCastStreamer.class, "(" + iAm + ") " + "Closing File.");
           
           closemyFileOutputStream();
           
         }
         myFile = new File("stream");
         myFile.mkdir();
         myFile = new File("stream" + File.separatorChar + s + ".mp3");
  ...

I stopped getting the "error" and execution continued until I could find "OK, I'm here." in the simplecenter.log.


Print Out Values, Whereever Feasible

A statement that says

if (debug) {
  Log.info( ShoutCastStreamer.class, "(" + iAm + ") " + "Updated ID3 tags. File is '" 
    + thisFileName + "'" );
}

goes much farther than if it said.

if (debug) {
  Log.info( ShoutCastStreamer.class, "Updated ID3 tags." );
}

'nuff said.

What's this if (debug)?

Again it's a matter of preference and style. Logging is good--especially for troubleshooting. It's bad, if your log is cluttered with logging info from 50 different sources. To find just the ones you're looking for becomes a chore.

I like to create a private boolean flag called debug in my code and set it to true. For items that I'd like to log now, but won't need it later except for when I'm troubleshooting, I enclose my Log.info statements in if (debug). One should be able to accomplish this by changing the log4j.properties file to not record info's and debugs as well. Your choice.


An Aside: Your Personality

Programmers are known for their extremely dry personalities. They usally have three or four letter names. Their greeting conversations are trite and usually sound like:

"Hello Bob, H2SO4!"
"Thanks Ken, Pi-r-squared to you too!  Don't synthesize anything I wouldn't synthesize."
"You know it, ... Bob.  Live long and prosper!" (Mr. Spock's hand gesture)

From that point on, they begin communicating only to exchange useful information and sound like two fax machines talking:

Ken: "r = 28.349343 J = 3343.1 radix = 10 bump:Alpha Alpha Omega"
Bob: "In that case, try { ½mv² = 40 N }, Tonight's Next Generation on channel 32 8:00pm"
Ken: "No, catch (WrongUnitsException e) { I think ½mv² = 32 J };  I knew that."
Rick: "Hey, your hair is on fire!  Meeting at 11:00."
Bob: "Who, mine?"

Obviously Rick is a managerial staff member...

Well, the simplecenter.log sort of looks like this as well. Go ahead, show a little personality (but only a little).

"Sheesh - error writing to outputstream!  Returning..."
"Bummer - ShoutCast ran out of data..."
"Oh man - Got a NumberFormatException.  Value was = 32,403 3"

Liven it up, just a little.  ;P

Concurrency Issues

Go ahead, click on any ShoutCast stream (just once) or select one from the DMS1 and watch the logs...

[31 May 2005 22:18:03,962] PCPlayer             Playing url: http://localhost:17984/media/4efdfc79.mp3 
[31 May 2005 22:18:04,152] ShoutCastStreamer    New instance 0 constructed. 
[31 May 2005 22:18:04,152] SimpleCenterModel    shoutcast was called 0 
[31 May 2005 22:18:04,152] ShoutCastStreamer    in get streaming url  I am instance 0 
[31 May 2005 22:18:05,023] ShoutCastStreamer    Returning URL: http://66.225.205.53:80  I am instance 0 
[31 May 2005 22:18:05,023] ShoutCastStreamer    Connection is: sun.net.www.protocol.http.HttpURLConnection:http://66.225.205.53:80  I am instance 0 
[31 May 2005 22:18:05,023] ShoutCastStreamer    Icy-Metadata request property is: ... 1  I am instance 0 
[31 May 2005 22:18:05,133] ShoutCastStreamer    Got input stream  I am instance 0 
[31 May 2005 22:18:05,133] ShoutCastStreamer    Line is : ICY 200 OK  I am instance 0 
[31 May 2005 22:18:05,133] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,133] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,133] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,143] ShoutCastStreamer    Reading ICY...  I am instance 0 
[31 May 2005 22:18:05,244] ShoutCastStreamer    s = null-null  I am instance 0 
[31 May 2005 22:18:05,244] ShoutCastStreamer    s1 = null-null  I am instance 0 
[31 May 2005 22:18:05,244] ShoutCastStreamer    Creating File Stream.  I am instance 0 
[31 May 2005 22:18:05,294] ShoutCastStreamer    New instance 1 constructed. 
[31 May 2005 22:18:05,294] SimpleCenterModel    shoutcast was called 1 
[31 May 2005 22:18:05,304] ShoutCastStreamer    in get streaming url  I am instance 1 
[31 May 2005 22:18:05,334] ShoutCastStreamer    Sheesh - error writing outputstream. Returning ( 0 ): Connection reset by peer: socket write error 
[31 May 2005 22:18:05,334] ShoutCastStreamer    I've closed the file.  I am instance 0 
[31 May 2005 22:18:05,334] ShoutCastStreamer    Less than 10 frames recorded on null-null.mp3. Deleting.  I am instance 0 
[31 May 2005 22:18:05,884] ShoutCastStreamer    Returning URL: http://66.225.205.53:80  I am instance 1 
[31 May 2005 22:18:05,884] ShoutCastStreamer    Connection is: sun.net.www.protocol.http.HttpURLConnection:http://66.225.205.53:80  I am instance 1 
[31 May 2005 22:18:05,884] ShoutCastStreamer    Icy-Metadata request property is: ... 1  I am instance 1 
[31 May 2005 22:18:06,225] ShoutCastStreamer    Got input stream  I am instance 1 
[31 May 2005 22:18:06,225] ShoutCastStreamer    Line is : ICY 200 OK  I am instance 1 
[31 May 2005 22:18:06,225] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,225] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,225] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,235] ShoutCastStreamer    Reading ICY...  I am instance 1 
[31 May 2005 22:18:06,335] ShoutCastStreamer    s = null-null  I am instance 1 
[31 May 2005 22:18:06,335] ShoutCastStreamer    s1 = null-null  I am instance 1 
[31 May 2005 22:18:06,335] ShoutCastStreamer    Creating File Stream.  I am instance 1 
[31 May 2005 22:18:07,126] MetadataLookup       Meta data lookup started


An odd thing happens--two instances of ShoutCastStreamer.class are fired up. One at 31 May 2005 22:18:04,152 and another at 31 May 2005 22:18:05,294. That's MORE THAN A SECOND later.

They both fire up, initialize and connect to ShoutCast and reads the headers and metadata and decodes the incoming song. But wait, the first instance (instance 0) reports an error at 22:18:05,334 and exits, while the second one continues to play (and records to disk).

So what does this prove? There might be concurrency issues; that there is more than one thread doing somthing to a common object or variable, causing unpredictable results. In SimpleCenter, everything is a new thread of some sort. Streaming runs as a background thread, new media scan runs as another background thread, as does my Orphan file checker. SimpleCenter has a nasty habit of starting up multiple copies of certain things (I haven't figured out why) but I've managed to cause only 1 copy of LaunchCast to start up instead of two (it was causing two files to begin recording but one would always crap out). In another class I had a static variable to a FileOutputStream class and was wondering why it was getting closed the moment it had opened up for recording.

I've added these class variables:

 // test variables (used to keep track of the number of instances
 // of this object).
 private static int instanceNumber = 0;
 private int iAm = 0;

I've added a bit of code on the constructors:

 public ShoutCastStreamer(String s, URL url1)
 {
   listeners = new ArrayList();
   url = url1;
   mediaId = s;
   
   // setup the preferences listener
   // registerPreferencesListener();
   
   // testing a quirk here---
   iAm = instanceNumber++;
   Log.info(ShoutCastStreamer.class, "New instance " + iAm + " constructed.");
   
 }  

and also wrote this finalizer:

 // finalizer - needed to close up any file handles before object is 
 // garbage collected.  Initially thought I'd need this to unhook
 // the PreferenceChangeListener until I discovered that there were
 // many instances of this object being spawned.  Really don't need
 // this in here...
 protected void finalize() throws IOException {
   
   if ( debug ) Log.info( ShoutCastStreamer.class, "Finalizer ( instance # " + iAm + " )");
   if (myFileOutputStream != null) {
   	myFileOutputStream.close();
   }
 }

Every so often, I punctuate the code with Log statements that indicate which instance of ShoutCastStreamer is performing what task:

   if (debug) {
       Log.info( ShoutCastStreamer.class, "(" + iAm + ") " + "Updated ID3 tags. File is '" + thisFileName + "'" );
   }

This enables me to see what has been happening in the logs, why, and to see what is stepping on it own toes.

The finalizer is important to me. In the lifecycle of an object, when an object becomes eligible for garbage collection, its finalizer is called (if one exists). There is no "default" finalizer, so if you have allocated references to resources outside the JVM, this is the place to release those resources so you avoid memory leaks.

The finalizer is also a great tool to tell me if some sort of error occurred that did not release the resource properly and thus it cannot become eligible for garbage collection (something is maintaining a reference to it), usually because of an improperly handled error somewhere.

Compilation Problems

You've been decompiling and looking at various classfiles, and might have even embarked on trying to edit them so they would recompile.

But you get to thinking that a previously recompiled file could use a little tweaking and so you edit it and compile it. Just one line, you know. Real small change...

BOOM! You're no longer able to compile your java file! It comes back with 52 errors!!!! You franticly undo your changes and you've still got 51 errors when you recompile!!!! It compiled just yesterday! Help!

If you've experienced something like the above, you're not alone. I've recently fallen into this trap on 6/6/2005 and posted a message saying I think I've clobbered my source file, so please wait while I re-decompile it so I can make the requisite changes...

What's happened is that your compiler tried to compile your java file A. In doing so, it checked dependencies on other class files and saw source code with newer dates than the binary and attempted to compile those and failed in doing so.

Look at your error messages Very Closely. Suppose you're compiling com\simpledevices\simplecenter\audio\medialibrary\MediaLibraryController.java, but your error messages include lines such as

.\com\simpledevices\simplecenter\audio\medialibrary\MediaLibraryContentControlle
r.java:353: undefined label: MISSING_BLOCK_LABEL_115
           break MISSING_BLOCK_LABEL_115;

and

.\com\simpledevices\simplecenter\common\contentdirectory\DeleteMediaAction.java:
159: cannot resolve symbol
symbol  : method _mthclass$ (java.lang.String)
location: class com.simpledevices.simplecenter.common.contentdirectory.DeleteMed
iaAction
perchance you meant '_mthclass.'
                    Log.error(DeleteMediaAction.class$com$simpledevices$simplece
nter$common$contentdirectory$DeleteMediaAction != null ? DeleteMediaAction.class
$com$simpledevices$simplecenter$common$contentdirectory$DeleteMediaAction : (Del
eteMediaAction.class$com$simpledevices$simplecenter$common$contentdirectory$Dele
teMediaAction = DeleteMediaAction._mthclass$("com.simpledevices.simplecenter.com
mon.contentdirectory.DeleteMediaAction")), e1);

Where did these come from? Simple: you've probably got a com\simpledevices\simplecenter\common\contentdirectory\DeleteMediaAction.java file and a com\simpledevices\simplecenter\audio\medialibrary\MediaLibraryContentController.java file that you've recently decompiled and was working to get them recompilable. Your MediaLibraryController.java depends on those class files for which you've produced recent source files.

Well you've got two options:

a) fix those two source files so they'll compile; then your MediaLibraryController.java will compile as well

b) rename those two source files (as long as the binaries are there) so that javac won't find them. You can edit those source at a later date.

Scary, but a small side-effect of the Compile In Place method.

Source Control

I ought to practice what I preach, but if I preach it here, perhaps I'll practice better.

This topic is more for me. I can only state it from a single programmer's point of view.

SimpleCenter.jar has gone through several release revisions. These revisions are applied outside of Simple Devices' numbering scheme. That is, on top of SimpleCenter v 2.0.3.0011 I have applied I, J, K and L updates.

Should Simple Devices come out with an update to SimpleCenter (let's say a v 2.1.0.0000) then all of the changes thus far will be clobbered when the new update is applied. For each of the classes that I've touched, I will have to check it against the 2.0.3.0011 (pure version) and the 2.1.0.0000 version to see if it's changed between the versions. If it hasn't, it might be safe to simply drop in the binaries that I've modified and compiled. Otherwise, I will have to decompile the new binary, apply the changes there ensuring that it still conforms to the logic and repackage the new binary and publish the new source.

Still with me?

Additionally, with each release, I will publish an archive (such as a zip) that contains the source that pertains to that change only. This is not infallible, but it's a start. The alternative is to include the cumulative batch of source files which is also not without its flaws. But got to pick a convention and stick with it, I guess...

Once multiple people get involved, then we'll have to resort to a single repository from which we will check code out. Because this is copyrighted code, there cannot be general access because parties OTHER than this forum will have access. Oh I grow dizzy just contemplating this project.

Personal tools