Programming in PHP: Axis Entity Object Helper

Our Axis Entity Object class is quite powerful and useful for lightweight ORM. But lets go ahead and create a help class that will alleviate a lot of business and view coding by automating the create and update forms and processing for Axis Entity Objects.

First lets look at how the code will operate in the business side of things. Here is how we generate the creation form for our example Axis Entity Object class “Fruit”:


<form action="index.php" method="post">
    <?php AxisObjectHelper::DrawFormCreate('Fruit'); ?>
    <input type="submit" />
</form>

What you can see here is that our helper class will contain a static method for creating an update form for our class “Fruit”. Lets take a look at that code.


    public static function DrawFormCreate($class)
    {
        // get our fields from our mapped database table
        $fields = AxisObjectHelper::GetFields($class);

        // loop through the returned fields and generate input fields matching the name of the columns.
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                echo '<div class="object_field" id="create_'.$class.'_field_'.$field_object->Field.'">'.self::PrettifyName($field_object->Field).'</div>';

                echo '<div class="object_input" id="create_'.$class.'_input_'.$field_object->Field.'">'.self::DrawInput($field_object).'</div>';
            }
        }
    }

Generating the create form is simple as pie once we have the corresponding columns of the class.

Here is how we process the create form:


AxisObjectHelper::ProcessFormCreate('Fruit');

So simple! Again we have a single line of PHP for processing the user’s input and creating the object. Lets take a closer look at that static method.


    public static function ProcessFormCreate($class)
    {
        // get our table columns
        $fields = AxisObjectHelper::GetFields($class);

        // create a new instance of our Axis Entity Object
        $create = new $class();

        // loop through the fields and create them
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $create->$field_name = $_POST[$field_name];
            }
        }

        return $create->Create();
    }

Using the Axis Entity Object class, we leverage it’s ORM to create the object very simply.

Updating objects is just as simple requiring only two lines of PHP to generate the form and to process it. Here is the example helper class in its entirety.


class AxisObjectHelper
{
    public static function OpenMySQL($host, $db, $user, $pass)
    {
        mysql_connect($host, $user, $pass);
        mysql_select_db($db);
    }

    public static function DrawFormCreate($class)
    {
        // get our fields from our mapped database table
        $fields = AxisObjectHelper::GetFields($class);

        // loop through the returned fields and generate input fields matching the name of the columns.
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                echo '<div class="object_field" id="create_'.$class.'_field_'.$field_object->Field.'">'.self::PrettifyName($field_object->Field).'</div>';

                echo '<div class="object_input" id="create_'.$class.'_input_'.$field_object->Field.'">'.self::DrawInput($field_object).'</div>';
            }
        }
    }

    public static function ProcessFormCreate($class)
    {
        // get our table columns
        $fields = AxisObjectHelper::GetFields($class);

        // create a new instance of our Axis Entity Object
        $create = new $class();

        // loop through the fields and create them
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $create->$field_name = $_POST[$field_name];
            }
        }

        return $create->Create();
    }

    public static function DrawFormUpdate($object)
    {
        $class = get_class($object);
        $primary_key_column = strtolower($class).'_id';

        $fields = AxisObjectHelper::GetFields($class);

        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $object_field_value = $object->$field_name;

                echo '<div class="object_field" id="update_'.$class.'_field_'.$field_object->Field.'_'.$object->$primary_key_column.'">'.self::PrettifyName($field_object->Field).'</div>';

                echo '<div class="object_input" id="update_'.$class.'_input_'.$field_object->Field.'_'.$object->$primary_key_column.'">'.self::DrawInput($field_object, $object_field_value).'</div>';
            }
        }
    }

    public static function ProcessFormUpdate($class, $object_id)
    {
        $fields = AxisObjectHelper::GetFields($class);
        $update_object = call_user_func(array($class, 'Factory'), $object_id, $class);

        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $update_object->$field_name = $_POST[$field_name];
            }
        }

        return $update_object->Update();
    }

    public static function DrawInput($field_object, $value = '')
    {
        // cases for input
        if(strpos($field_object->Type, 'tinyint') !== false && strpos($field_object->Type, '(1)') !== false)
        {
            if($value != '')
            {
                if((bool)$value)
                {
                    return '<input type="checkbox" class="input_boolean" name="'.$field_object->Field.'" value="1" checked="checked" />';
                }
            }

            return '<input type="checkbox" class="input_boolean" name="'.$field_object->Field.'" value="1" />';
        }

        if  (
            strpos($field_object->Type, 'int') !== false ||
            strpos($field_object->Type, 'varchar') !== false ||
            strpos($field_object->Type, 'float') !== false ||
            strpos($field_object->Type, 'decimal') !== false ||
            strpos($field_object->Type, 'double') !== false ||
            strpos($field_object->Type, 'real') !== false ||
            strpos($field_object->Type, 'date') !== false ||
            strpos($field_object->Type, 'time') !== false
            )
        {
            return '<input type="text" class="input_varchar" name="'.$field_object->Field.'" value="'.htmlentities($value).'" />';
        }

        if(strpos($field_object->Type, 'text') !== false)
        {
            return '<textarea class="input_text" name="'.$field_object->Field.'">'.$value.'</textarea>';
        }

    }

    public static function PrettifyName($name)
    {
        return ucwords(str_replace('_' , ' ', $name));
    }

    // MySQL Specific
    public static function GetFields($class)
    {
        $primary_key_column = strtolower($class).'_id';
        $table = DB_T_PREPEND.strtolower($class);

        $cmd = "SHOW COLUMNS FROM ".$table."";

        $result = mysql_query($cmd);
        while($row = mysql_fetch_object($result))
        {
            $cols[] = $row;
        }

        return $cols;;
    }

}

This helper class is simply used as an example for automating CRUD using Axis Entity Objects. We can easily modify the methods apply business rules for validating the object by providing that method in our “Fruit” class and calling its Validate() method. We can also adjust the methods for other databases.

That is our limitation with this example helper. Each database has different ways of parsing the columns of a table. This example was specific to MySQL.

Programming in PHP: Axis Entity Objects

During some spare time I developed an abstract ORM class that can be extended by entity classes that automatically implement the create, update and delete methods for the object’s table entry in a relational database. This is a work in progress, but I will go through the existing version to explain.

The create, update, and delete methods can be called on any object that extends the AxisEntityObject class to perform the related operation on that instance. It will immediately remove, create or update the object related entry in the database using created SQL and PHP PDO. But also, each class inherits a static method that is the factory for the object type which requires only the primary key associated with an object to generate it.

Lets take a look at the interface.

<?php
interface IAxisEntityObject{   
    public function Create();
    public static function Factory($primary_key, $custom_class_name = '');
    public static function FactoryList($primary_keys, $custom_class_name = '');
    public static function FactoryAll();       
    public function Update();
    public function Delete();
}

As you can see we require the defined methods and also require three types of factories that are common in ORM object generation. We can product all objects of a class, a single object by primary key, or a list based on an array of primary keys.

Here is the implementation of the abstract class.


abstract class AxisEntityObject implements IAxisEntityObject
{

    //////////////////////////////////
    // CUD METHODS
    ////////////////////////////////

    public function Create()
    {
        // We need to grab the current objects class before we call its functions
        // so that we can link to the related table and not to a table named after our abstract class

        $class = get_class($this);
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        $entry_fields = array();

        // Loop through each of the fields of the object instance
        // and look for any other properties that need to cascade the creation process

        foreach($this as $field => $value)
        {
            if($field != $primary_key_column)
            {
                if($value instanceof AxisEntityObject)
                {
                    $value->Create();
                }
                elseif(is_array($value))
                {
                    foreach($value as $array_value)
                    {
                        if($array_value instanceof AxisEntityObject)
                        {
                            // Cascade call all properties Create method if they are of the same class type
                            // recursive in nature

                            $array_value->Create();
                        }
                    }
                }
                elseif($this->$field != null)
                {
                    // Build our SQL

                    $k_sql .= $field.', ';

                    $v_sql .= ':'.$field.', ';

                    $entry_fields[$field] = $value;
                }
            }
        }

        // Use PHP PDO to persist the objects fields and complete the create method
        $cmd = "INSERT INTO ".$table." (".substr($k_sql, 0, -2).") VALUES (".substr($v_sql, 0, -2).")";

        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->execute((array)$entry_fields);

        return $GLOBALS['DBH']->lastInsertId();
    }

    public function Update()
    {
        // Again for updates we need the class name that extends AxisEntityObjects
        $class = get_class($this);
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        // Cascade the update call to all properties that are also extend the abstract class

        if($this->$primary_key_column != '')
        {

            foreach($this as $field => $value)
            {
                if($field != $primary_key_column)
                {
                    if($value instanceof AxisEntityObject)
                    {
                        $value->Update();
                    }
                    elseif(is_array($value))
                    {
                        foreach($value as $array_value)
                        {
                            if($array_value instanceof AxisEntityObject)
                            {
                                $array_value->Update();
                            }
                        }
                    }
                    elseif($value != null)
                    {
                        $v_sql .= $field . ' = :'.$field.', ';

                        $entry_fields[$field] = $value;
                    }
                }
            }

            // Persist the object changes
            $cmd = "UPDATE ".$table."  SET ".substr($v_sql, 0, -2)." WHERE ".$primary_key_column." = ".$this->$primary_key_column;

            $STH = $GLOBALS['DBH']->prepare($cmd);
            $STH->execute((array)$entry_fields);
        }
    }

    public function Delete()
    {
        $class = get_class($this);
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        if($this->$primary_key_column != '')
        {
            $cmd = "DELETE FROM ".$table." WHERE ".$primary_key_column." = ".$this->$primary_key_column;

            $STH = $GLOBALS['DBH']->prepare($cmd);
            $STH->execute((array)$this);
        }
    }

    //////////////////////////////////
    // RETRIEVE METHODS
    ////////////////////////////////

    public static function Factory($primary_key, $custom_class_name = '')
    {
        // Get our proper class name and table names
        if($custom_class_name != '')
            $class = $custom_class_name;
        else
            $class = get_called_class();
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        // get table entry for object
        $cmd = "SELECT
                    *
                FROM
                    `".$table."`
                WHERE
                    `".$primary_key_column."` = '".$primary_key."'";

        // Execute call
        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->setFetchMode(PDO::FETCH_CLASS, $class);
        $STH->execute();
        $result = $STH->fetch(PDO::FETCH_CLASS);

        if($result == false)
            return null;

        return $result;
    }

    public static function FactoryAll()
    {
        // get the proper class
        $class = get_called_class();
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        $cmd = "SELECT ".$primary_key_column." FROM ".$table;

        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->execute();

        // Call our Factory method on all returned objects mapped
        while($row = $STH->fetch(PDO::FETCH_OBJ))
        {
            $list[] = call_user_func(array($class, 'Factory'), $row->$primary_key_column, $class);
        }

        return $list;
    }

    public static function FactoryList($primary_keys, $custom_class_name = '')
    {
        // get our proper class
        if($custom_class_name != '')
            $class = $custom_class_name;
        else
            $class = get_called_class();
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        $in = "('".implode("', '", $primary_keys)."')";

        // get table entries
        $cmd = "SELECT
                    *
                FROM
                    `".$table."`
                WHERE
                    `".$primary_key_column."` IN ".$in."
                ORDER BY
                    CASE
                    ";
        foreach($primary_keys as $i => $v)
        {
            $cmd .= " WHEN ".$primary_key_column." = ".$v." THEN ".$i." ";
        }

        $cmd .= " END";

        // USE PDO to build the objects of the proper class

        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->setFetchMode(PDO::FETCH_CLASS, $class);
        $STH->execute();

        while($row = $STH->fetch(PDO::FETCH_CLASS))
        {
            $list[] = $row;
        }

        return $list;
    }

    // These methods are used to map the table names and class names for the ORM logic
    protected static function primaryKeyName($class)
    {
        return call_user_func(array($class, 'underscoreFromCamelCase'), $class) . '_id';
    }

    protected static function tableName($class)
    {
        return DB_T_PREPEND . call_user_func(array($class, 'underscoreFromCamelCase'), $class);
    }

    protected static function underscoreFromCamelCase($str)
    {
        $str[0] = strtolower($str[0]);
        $func = create_function('$c', 'return "_" . strtolower($c[1]);');

        return preg_replace_callback('/([A-Z])/', $func, $str);
    }
}

This class will perform ORM operations on an instance and can be used very simply in the following ways.

Lets extend this class with a normal entity class like Fruit.

Entity Class Creation:


class Fruit extends AxisEntityObject
{

}

Simple!

Create:


$fruit = new Fruit();
$fruit->fruit_name = 'Pineapple';
$fruit_id = $fruit->Create(); 

Retrieve and Update:


$fruit = Fruit::Factory('21'); // 21 is our primary key id in the database.

Combine retrieval with updating for simple object updating.


$fruit = Fruit::Factory('21');
$fruit->name = 'Orange';
$fruit->Update();

Delete:


$fruit = Fruit::Factory('21'); // 21 is our primary key id in the database.
$fruit->Delete();

Programming in C++: Final Forecast

Final Forecast is a networked terrain simulation video game developed using C++, the Ogre game libraries and the Boost C++ libraries. It utilizes threads, network communication, 3D graphics acceleration and similar game concepts. Designed by myself and my software engineering senior design team Final Forecast as presented in the University of Texas at Dallas Senior Design showcase for industry professionals and academic staff. Let me go over some of the methods employed and some of the code involved.

Developed for both the linux and windows platforms the code is separated into two different projects: the server and the client.

The Server:

More coming soon.

Case Study: Museum Works Audio Tours

Museum Works Audio Tours is a company that provides multimedia presentations to captivate and interest museum visitors for exhibits of all types. Much of their presentation includes audio messages and ambient sounds to represent the exhibit to users of the museum.

Museum Works Audio Tours contracted Angelwire Creative to build a media management back end system for their existing web site. I was brought in to design and develop the tools they needed a management system that allowed them to upload audio files of type mp3, wav, or aiff. They as well needed to correlate a visual image with each audio file for their viewers. The management system needed to present the files of their choosing to the visitors of their site and present them with a UI tool for navigating the audio files each with their own cover image.

This was incorporated for them through a back end management system that is still being used today.

Technologies Employed:

  • PHP
  • Quicktime Player
  • PHP GD Library

Case Study: Xeta Telecommunications

Xeta Telecommunications was in need of an eCommerce solution customized to their own specific business model. This meant an eCommerce store that allowed for a tier based pricing and allocating users into particular pricing brackets. It also meant a private web store that only specific clientele could access. All of this was needed and it was needed quickly and within a limited budget. I was contracted through Angelwire Creative to help achieve this goal.

To accommodate this project a customized version of osCommerce was employed that would involve a heavy modification of the core application. Since osCommerce was an open source project, it allowed myself to make the needed changes to the code base for Xeta, while at the same time eliminating the need for developing all the other necessary eCommerce tools such as user management, product management, shopping carts, and payment APIs.

Strike Iron

Portions of the eCommerce store leveraged the powerful API provided by Strike Iron at www.strikeiron.com. Their API allowed for customer zip code look-ups to determine the user’s zip code based upon their designated street address, and also resolve possible city conflictions regarding zip codes that overlap zip code boundaries. It also allowed for validating customer addresses, phone numbers, and was used to calculated tax rates based on customer location.

Technologies Employed:

Case Study: Nations Home Warranty

Nations Home Warranty is a Texas based home warranty company servicing native Texans and Oklahomans with outstanding home warranty services. Much of their great customer interaction is supported by a well-built and maintained application process on their website home-warranty.com.

The Nations Home Warranty was in a serious need up upgrade and contemporizing to capture their visitors attention and provide them with tools for managing their home warranty contacts. I was employed through Angelwire Creative to redesign an existing but dated application process that had become un-maintainable by their staff. Much of the PHP was designed for PHP 3 and needed a serious redesign if it were to survive a single server update.

The application process was not only streamlined for PHP 4 at the time, it was also rebuilt to be modular with adjustable pricing and pricing packages for their various contracts. It also was integrated into a user toolkit that users could use to review their existing contracts and update their contact information. As well, the application process was built to service individuals who spoke both English and Spanish as a first language.

This was made possible by refactoring the site’s text into manageable language files that could be swapped out as the user defined their language. It would allow for the expansion of any number of languages as well.

An application management center was developed for the Nations Home Warranty staff to review new applications, archive or remove outdated applications, and contact their customer base. As well, support was given for the staff to issue newsletters to their subscribed email base.

 


 

Case Study: The Voice of Pro Equine Group

The Voice of Pro Equine group was a project developed for the internal use of Pro Equine Group of Dallas, TX. I was contracted to develop this application through Angelwire Creative. It accomplished many goals that the company needed to complete and is in active use today.

The Problem:

Pro Equine Group was a parent company over several smaller companies that sold and managed products regarding equestrian and riding uses. Each of these smaller companies managed and oversaw their own repository of customer contacts for use in future marketing and sales information. The problem lay in the fact that many of these customers purchased products among several of their child companies at each time, and since each child company oversaw their own database of customer information, linking and managing the customers was an impossibility.

Each child company would store their customer database using a different format and different structure. Some stored their information use Excel, others a database. All stored them differently.

The Solution:

I developed a system for managing not only their child companies, but also created a powerful series of tools that alleviated the separated customer databases. A powerful upload tool allowed their user base to upload any CSV of any format containing customers and any other secondary customer information into a single database that Pro Equine Group could manage and use for marketing.

This was accomplished by developing a front-end tool that allowed users of a specific user type the power to create custom columns in a report table within a single database. They could designate each of these columns to contain a different type of data and provide a related description and name. These columns represented other customer information such as receipt information, age ranges etc.

In correlation to this, a powerful upload tool allowed them to map any columns in a CSV to the columns defined in the database table. These mappings could be saved for re-use by the user and could be shared across the chile company to which that user belonged.

Uploading the CSV against this mapping would send the information through a filtering system that would recognize duplicated customers within the upload and would either mark that entry as “to merge”, “conflicted” or “matched”. Each option resulted in a protocol that could allow administrators to handle the confictions or merging process.

In addition to the upload procedure, a tool was developed to present a Graphical User Interface to the user that would allow them to shape and create their own queries into the uploaded customer database. The results would print in an HTML tabular format with support for downloading into Excel. These reports could be saved in the form of “campaigns” that would be re-usable and re-reportable by users for marketing usage or anything desired.

These tools allowed Pro Equine group the ability to overcome their separated customer database problems with ease. But alongside of this solution a fully functioning user management, customer search tool, and company management tool allowed Pro Equine group to really manage their business better than before.

Technologies Employed:

  • PHP & MySQL
  • CSV parsing
  • PHP Mailer
  • Microsoft Excel
  • JavaScript & jQuery

Programming In Shell Script: Count Files

Here is a sample shell script program that simply counts the number of regular files, directories and symbolic links within a directory, but excludes counting items within subdirectories.


# The script accepts a directory and produces output that tells how many regular files, directories, and symbolic links are in the directory (exclude subdirectories).
# The words in the output are singular or plural depending on the count (e.g. 1 link, 2 links).  Make sure the directory exists.
# Use the current directory if none was provided.
# Print out a "Usage" message if the user gave more than one argument.

# Syntax:   countfiles.sh directory

#!/bin/bash

#usage
directory=$1
[ $# -eq 0 ] && directory=$PWD
[ $# -gt 1 ] && { echo "Usage: countfiles.sh requires one directory as a parameter."; exit 1; }

dirs=0;files=0;links=0
# take directory parameter and check if it exists
# $0 is the directory
[ -d $directory ] || { echo "Directory specified either does not exist or is of a different file type."; exit 1; }

# loop through files in directory
for file in $directory/*;
do
	[ -d "$file" ] && dirs=`expr $dirs + 1`
	[ -f "$file" ] && files=`expr $files + 1`
	[ -h "$file" ] && links=`expr $links + 1`
done

echo "Directory $directory contains:";
if [ $dirs -eq 1 ]; then
	echo "$dirs directory"
else
	echo "$dirs directories"
fi
if [ $files -eq 1 ]; then
	echo "$files file"
else
	echo "$files files"
fi
if [ $links -eq 1 ]; then
	echo "$links symbolic link"
else
	echo "$links symbolic links"
fi

exit 0

Here is a test:

jordan@jordan-VirtualBox:~/Desktop$ mkdir testDir
jordan@jordan-VirtualBox:~/Desktop$ cd testDir
jordan@jordan-VirtualBox:~/Desktop/testDir$ touch regular_file
jordan@jordan-VirtualBox:~/Desktop/testDir$ mkdir directory
jordan@jordan-VirtualBox:~/Desktop/testDir$ ln -s ~/Desktop/testDir/regular_file symbolic_link
jordan@jordan-VirtualBox:~/Desktop/testDir$ ls -l
total 4
drwxr-xr-x 2 jordan jordan 4096 2011-12-14 11:10 directory
-rw-r--r-- 1 jordan jordan    0 2011-12-14 11:10 regular_file
lrwxrwxrwx 1 jordan jordan   41 2011-12-14 11:11 symbolic_link -> /home/jordan/Desktop/testDir/regular_file
jordan@jordan-VirtualBox:~/Desktop/testDir$ cd ../
jordan@jordan-VirtualBox:~/Desktop$ ./countfiles.sh testDir
Directory testDir contains:
1 directory
2 files
1 symbolic link

After creating our test folder, the outputs of count files performs well. Even though a link is different than a regular file, it is still a file all the same, so we have a count of 2 files in our report.

Extremely simple but extremely useful to have as a nice command line tool. Make an alias for it in your Linux environment and simply run “countfiles.sh”. If the directory argument is blank, it will use the current directory.

Download: countfiles.zip

Programming In Shell Script: Merge Directories

A couple of years ago I was tasked with writing a script for merging the files within two directories into a new directory. This could be accomplished through a variety of scripting languages like Perl but I thought it was be good time to work on some Linux/Unix shell scripting.

Here is the program in its entirety broken into sections:


# The script merges the regular files in directory1 and directory2 into destdir (exclude subdirectories).  done
# Files that are in only one of the directories may be copied directly.  done
# However, if the file is in both directories, copy the newer file.  done
# Make sure the source directories exists.  done
# Create the destination directory if it does not exist.  done
# Print out a "Usage" message if the user did not provide 3 arguments. done

# Syntax:   merge.sh directory1 directory2 destdir

#!/bin/sh

dir1=$1; dir2=$2; desdir=$3;

# Usage
[ $# -ne 3 ] &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; { echo "Usage: merge.sh requires 3 directory parameters.\nSyntax: merge.sh directory1 directory2 destdir\nmerge.sh takes all regular files from two directories and copies them into one destination directory.\nIf each directory has the same file, the newer one is copied over."; exit 1; }

# make sure Dir 1 ad Dir 2 exist
exs=0 # 0 = both present | 1 = dir 1 missing | 2 = dir 2 missing | 3 = both dir 1 and dir 2 missing
[ ! -d $dir1 ] &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; exs=`expr $exs + 1`
[ ! -d $dir2 ] &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; exs=`expr $exs + 2`

[ $exs -eq 1 ] &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; { echo "Directory one cannot be found"; exit 1; }
[ $exs -eq 2 ] &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; { echo "Directory two cannot be found"; exit 1; }
[ $exs -eq 3 ] &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; { echo "Directory one and directory two cannot be found"; exit 1; }

This initial part of the program does our argument validation and determines if the two directories we wish to merge are real directories.


# make the destdir if it does not exist
# Check if a file with this name exists
# if so:
	# check if it is a directory
	# if so:
		# continue
	# if not:
		# error, we would overwrite another file with that name
# if not:
	# create this directory

if [ -e $desdir ]; then
	if [ ! -d $desdir ]; then
		echo "Your specified destination directory exists as a file that is not a directory.\nThis program will not overwrite this file.\nProgram end."
		exit 1
	fi
else
	mkdir $desdir
fi

The above portion checks the target directory for the merged files to be transferred two. If the directory does not exist it is created.


# Loop through Directory 1

	# check if current file is a FILE
	# if so:
		# check for existance of file in Directory 2
		# if exists:
			# compare last modified time of each file
			# copy file into the Destination Directy which has the latest modification
		# if no exist:
			# copy file directly
	# if not:
		# ignore file

for d1file in $dir1/*; # $d1file is like: dir1/file
do
	curFile=`basename $d1file`
	if [ -f "$d1file" ]; then
		if [ -f "$dir2/$curFile" ]; then
			if [ "$d1file" -nt "$dir2/$curFile" ]; then
				cp "$d1file" "$desdir"
			else
				cp "$dir2/$curFile" "$desdir"
			fi
		else
			cp "$d1file" "$desdir"
		fi
	fi
done

# at this point we have taken all unique files from directory one and moved them into the destination directory.
# files which were in both, have been moved from either directory 1 or directory 2 depending on which one is newer
# directory 2 still has files which are unique to it, and they must be moved

We then loop through directory one and copy all of the files into the destination directory.


# Loop through Directoy 2

	# check if current file is a FILE
	# if so:
		# Check if file has been moved into Destination Directory already (from Dir 1 loop)
		# if so:
			# ignore file
		# if not:
			# copy file into Destination Directory
	# if not:
		# ignore file

for d2file in $dir2/*;
do
	curFile=`basename $d2file`
	if [ -f "$d2file" ]; then
		if [ ! -e "$desdir/$curFile" ]; then
			cp "$d2file" "$desdir"
		fi
	fi
done

# at this point all files which were also in Dir 1 and were newest were copied by Loop 1
# unique files in Dir 2 have ben copied into Destination Directory

exit 0;

Same is done for directory two leaving us with all the files of both directories copied into a new directory of our choosing. Lets see it in action:

jordan@jordan-VirtualBox:~/Desktop$ mkdir dir1;
jordan@jordan-VirtualBox:~/Desktop$ touch dir1/A;
jordan@jordan-VirtualBox:~/Desktop$ touch dir1/B;
jordan@jordan-VirtualBox:~/Desktop$ touch dir1/X;
jordan@jordan-VirtualBox:~/Desktop$ mkdir dir2;
jordan@jordan-VirtualBox:~/Desktop$ touch dir2/C;
jordan@jordan-VirtualBox:~/Desktop$ touch dir2/D;
jordan@jordan-VirtualBox:~/Desktop$ touch dir2/X;
jordan@jordan-VirtualBox:~/Desktop$ echo "dir1 X file" > dir1/X;
jordan@jordan-VirtualBox:~/Desktop$ echo "dir2 X file" > dir2/X;
jordan@jordan-VirtualBox:~/Desktop$
jordan@jordan-VirtualBox:~/Desktop$ ls -l
total 12
drwxr-xr-x 2 jordan jordan 4096 2011-12-14 10:58 dir1
drwxr-xr-x 2 jordan jordan 4096 2011-12-14 10:58 dir2
-rwxrwxrwx 1 jordan jordan 3321 2009-04-04 15:18 merge.sh
jordan@jordan-VirtualBox:~/Desktop$

Above I have created a test environment for the script. We have two directories, each with unique files and each with one file “X” with the same name but differing content. Now I will run the merge.sh shell script:

jordan@jordan-VirtualBox:~/Desktop$ ./merge.sh dir1 dir2 dest
jordan@jordan-VirtualBox:~/Desktop$
jordan@jordan-VirtualBox:~/Desktop$ ls -l
total 16
drwxr-xr-x 2 jordan jordan 4096 2011-12-14 11:00 dest
drwxr-xr-x 2 jordan jordan 4096 2011-12-14 10:58 dir1
drwxr-xr-x 2 jordan jordan 4096 2011-12-14 10:58 dir2
-rwxrwxrwx 1 jordan jordan 3321 2009-04-04 15:18 merge.sh
jordan@jordan-VirtualBox:~/Desktop$ cd dest
jordan@jordan-VirtualBox:~/Desktop/dest$ ls -l
total 4
-rw-r--r-- 1 jordan jordan  0 2011-12-14 11:00 A
-rw-r--r-- 1 jordan jordan  0 2011-12-14 11:00 B
-rw-r--r-- 1 jordan jordan  0 2011-12-14 11:00 C
-rw-r--r-- 1 jordan jordan  0 2011-12-14 11:00 D
-rw-r--r-- 1 jordan jordan 12 2011-12-14 11:00 X
jordan@jordan-VirtualBox:~/Desktop/dest$
jordan@jordan-VirtualBox:~/Desktop/dest$ cat X
dir2 X file
jordan@jordan-VirtualBox:~/Desktop/dest$

As you can see, both directories merged as expected and the matched file “X” was taken from “dir2″ versus “dir1″ because “dir2″ files were copied after “dir1″ files.

Though the program is simple in nature, it fits the concept of shell scripting very nicely. Developing large applications is not really suited well for shell scripting, but making smaller tools that can be used consistently like many of Unix tools is of extreme benefit to the programmer.

Download the script and try it out yourself.

Download: merge.zip

Programming in Java: Socket Gaming

Lets take a look at an example of using Java for communication across a network. The program being demonstrated uses a server and a client program to network a game of “Wheel of Fortune” between 3 players.

Each “player” is represented by a class Player and is a thread on the server. This class also does the bulk of the logic. The reason for designing it this way is that we can easily scale the server to any number of players and also reuse several pieces of our code.

Let’s take a look at our Server.main():


        public static void main(String[] args)
	{

		  ////////////////////////////////
		 //    ARGUMENT ACCEPTANCE     //
		////////////////////////////////

		// Argument Protection: avoid any non-whole number strings.
		try
		{
			//- Avoid blank arguments.
			if(args[0].equals("")){
				System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
				System.exit(0);
			}
			//- Avoid negative and zero arguments.
			if(Integer.parseInt(args[0]) < 0){
				System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
				System.exit(0);
			}
		}
		catch(NumberFormatException e)
		{
			//- Avoid arguments that are not numeric.
			System.out.println("Usage Error: invalid argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
			System.exit(0);
		}
		catch(ArrayIndexOutOfBoundsException e)
		{
			//- Avoid arguments that are not existent.
			System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
			System.exit(0);
		}

		// Save the passed port number
		int port = Integer.parseInt(args[0]);

		// Create our Server object
		Server server = new Server(port);

		// Initiate game
		server.listenSocket();

		// Close game and server
		System.exit(0);
	}

Outside of the normal argument validation the main only initiates one line of code to run the game: server.listenSocket();. This method performs all of the game creation code such as player joining etc. Lets look at Server.listenSocket() now:


	public void listenSocket()
	{

		  ///////////////////////////////
		 // CREATE SERVER
		///////////////////////////////
		try
		{
			server = new ServerSocket(this.port);
		}
		catch (IOException e) {
			System.out.println("Could not listen on port " + this.port);
			System.exit(-1);
		}

		  ///////////////////////////////
		 // CREATE PHRASE
		///////////////////////////////

		// Read the phrases file into an array
		this.phrases = readFile("phrases.txt");

		// Pick a random phrase line from the file
		Server.phrase = this.phrases.get(generator.nextInt(this.phrases.size()));

		// Split the phrase line by tab
		this.phrase_split = Server.phrase.split("\t");

		// Save our phrase
		Server.phrase = this.phrase_split[0];

		// Save the phrase's category
		Server.category = this.phrase_split[1];

		// Create our hidden phrase to display made of hyphens
		Server.displayPhrase = "";
		for(int i=0; i < Server.phrase.length(); i++)
		{
			// IF there is a space, make it shown
			if(Server.phrase.charAt(i) == ' '){
				Server.displayPhrase = Server.displayPhrase.concat(" ");
			}
			// For each letter represent it with a -
			else{
				Server.displayPhrase = Server.displayPhrase.concat("-");
			}
		}

		  ///////////////////////////////
		 // ACCEPT PLAYERS
		///////////////////////////////

		System.out.println("Server initiated and game is set. Awaiting players...");

		// Keep waiting until all players have joined
		while(true)
		{
			// Temporary player object
			Player p;
			try
			{
				// Create our player object for the client joining
				p = new Player(server.accept(), playerCount, openPlay);

				// Turn that player object into a thread
				Thread t = new Thread(p);

				// Add the player to the player list
				players_o.add(playerCount, p);

				// Add the thread to the player thread list
				players.add(playerCount, t);

				// Notify server of new player
				System.out.println("Player " + playerCount + " Accepted!");
				playerCount++;

			}
			catch (IOException e){
				System.out.println("Accept failed: " + this.port);
				System.exit(-1);
			}

			// IF we have all three players needed, stop accepting more players
			if(players.size() >= 3){
				System.out.println("Maximum amount of players have joined.");
				break;
			}
		}

		// Start the threads running, therefore starting the game
		System.out.println("Game has begun.");
		for(int j=0; j < playerCount; j++)
		{
			players.get(j).start();
		}

		// Once the threads are done the game is done
		// The server simply needs to wait until the player threads have completed their game
		for(int j=0; j < playerCount; j++)
		{
			// Close down the thread when it is complete
			try{
				players.get(j).join();
			} catch (InterruptedException e){}
		}

		// Notify the server that the game is over
		System.out.println("Game is over.");
	}

Server.listenSocket() operates in the following steps:

  1. First it opens the port set in the argument for listening.
  2. The server then uses other logic to parse a random phrase for the game from a file
  3. Once that is complete, the server awaits the players to connect.
  4. Each time a player connects, it creates an instance of the Player class but does not begin their thread immediately.
  5. After all players have joined, the threads for each player are started in the order by which they have joined
  6. At that point the logic of the Server class is done, and it simply idles until each player’s thread has closed.

So the server is quite simple in that is sets up the game and dictates when it should end and begin. Like I said previously, the real logic of the game lies within the Player class threads. Lets take a look at some pieces of that code:


///////////////////////////////
// ESTABLISH CONNECTION TO CLIENT
///////////////////////////////
try
{
	// Create our Communication Tools
	in = new BufferedReader(new InputStreamReader(player.getInputStream()));
	out = new PrintWriter(player.getOutputStream(), true);
}
catch (IOException e) {
	System.out.println("in or out failed");
	System.exit(-1);
}

The Player.run() method for the thread starts with establishing a connection to the client over the network. The reference to “player” is actually an instance of the Java class Socket that was passed into the constructor of the player instance by the server. This code merely demonstrates that each player instance on the server is a one-to-one relationship with a client across a network.


// Continue running the game until the game has been ended
while(Server.gameOn){

	// THE FOLLOWING IS A CRITICAL SECTION AND SHOULD ONLY BE RUN IF IT IS THE PLAYERS TURN
	// IF it is the player's turn to play
	if(Server.playerTurn == playerId){

		// Utilize the Semaphore to ensure no one else can play
		// WAIT (openPlay);
		try{
			openPlay.acquire();
		}catch (InterruptedException e){ System.out.println("wait(openPlay) failure"); }

		// Announce to the player that it is their turn
		Server.announce("Player " + this.playerId + "'s turn...", this.playerId);

		// Loop until turn has ended
		while(true){

The next section of code is more or less the game loop. Each player thread will loop continually while the game is on, but will not continue logically until until it is their turn as designated by the serve. Once it becomes the player’s turn an internal game play loop begins. Here is an overview of the internal player loop:


			// IF the player has not made a spin
			if(this.spin == 0)
			{
				// Ask the player to spin the wheel
				out.println("respond@It is your turn. Type \"spin\" to spin the wheel.");
				try {
					line = in.readLine().trim();
				}
				catch (IOException e) {
					System.out.println("Read failed");
					System.exit(-1);
				}

				// IF the player has spun the wheel
				if(line.equals("spin")){

					// Choose a random value for the spin wheel
					this.spin_val = generator.nextInt(24);

					// CODE removed for tutorial but it contains logic for three possible spins:

					// Bankrupt and Lose Turn
					if(this.spin_val == 22){
						...
					}
					// Else If Lose Turn
					else if(this.spin_val == 23){
						...
					}
					// Else Save the dollar amount spun
					else{
						...
					}
				}
				else{
					this.spin = 0;
				}
			}
			// The player has already spun the wheel and is ready to play
			else{
				// CODE removed for tutorial but it contains logic for three possible spins:

				// IF the player has not made a choice for their turn
				if(this.decision == 0 || this.decision > 3){

					// Provide the player with the menu to choose from
					// 1. Guess the Phrase
					// 2. Buy a vowel
					// 3. Guess a consonant
					...

				}	

				// IF the player has decided to "Guess phrase"
				else if(this.decision == 1){

					// Ask the player for their guess at the phrase
					...

					// Check to see if the player's guess was right

					// IF the player is correct
					if(line.toLowerCase().equals(Server.phrase.toLowerCase())){

						// Award the player their money and announce their victory to waiting players
						out.println("null@Phrase \"" + line + "\" is correct! You win $" + this.budget + "!");
						Server.announce("Player " + this.playerId + " has won the game and $" + this.budget + " by guessing \"" + line + "\".", this.playerId);

						// Turn off the game
						Server.gameOn = false;
						break;
					}
					// IF the player was wrong with their guess
					else{

						// Notify them and the other players they were wrong
						out.println("null@Phrase \"" + line + "\" is incorrect. Your turn is over.");
						Server.announce("Player " + this.playerId + " guessed \"" + line + "\", but was wrong.", this.playerId);

						// Cause the player's turn to end
						// Unset their spin and decision
						// Move to the next player
						// Release the critcal section
						this.spin = 0;
						this.decision = 0;
						this.nextPlayerTurn();
						openPlay.release();
						break;

					}
				}	

				// IF the player decided to "Buy a vowel"
				else if(this.decision == 2){

					// Ensure the player has enough money to buy a vowel
					if(this.budget >= 250){

						// Ask the player which vowel they want
						...

						// IF there is a single letter that is a vowel
						if(line.length() == 1){

							// While checking if the letter has been guessed already, add it to the guessed list
							if(!this.addCheckLetter(line)){

								// Lookup the number of instances the letter appears in the phrase
								letterLocations = findLetterOccurances(line, Server.phrase);

								// IF there are letter occurances
								// The player is CORRECT
								if(letterLocations.size() > 0){

									// Remove the cost of the vowel from their budget
									...

									// Notify the player and others of their correct guess
									...

									// Update the hidden phrase, showing the vowel
									// Notify everyone of the new, partially exposed, phrase
									...

									// The player retains their turn
									// Reset their decision and spin and keep control of critical section
									...

								}
								// IF there are no letter instances
								// The player is INCORRECT
								else{

									// Notify the player and others of the mistake
									...

									// Cause the player to lose their turn
									// unset their spin and decision
									// move to the next player
									// and release the critical section
									...

								}
							}
							// IF the vowel has already been guessed before
							else{
								// ask again
								...
							}
						}
						// IF the player did not respond with a single letter vowel
						else{
							...
						}

					}
					// IF the player did not have enough money to purchase a vowel
					else{
						..
					}
				}

				// IF the player chose to "Guess a consonant"
				else if(this.decision == 3){

					// Ask the player for the consonant guess
					...

					// IF the consonant response is valid
					if(line.length() == 1){

						// While checking if the letter has been guessed already, add it to the guessed list
						if(!this.addCheckLetter(line)){

							// Lookup the number of instances the letter appears in the phrase
							letterLocations = findLetterOccurances(line, Server.phrase);

							// IF there are letter occurances
							// The player is CORRECT
							if(letterLocations.size() > 0){

								// Reward the player with their spin money times the number of letter occurances
								...

								// Notify the player and others of the correct guess
								.

								// Update the hidden phrase, showing the vowel
								// Notify everyone of the new, partially exposed, phrase
								...

								// The player retains their turn
								// Reset their decision and spin and keep control of critical section
								...

							}
							// IF the player's guess was wrong
							else{	

								// Notify the player and others of the mistake
								...

								// Cause the player to lose their turn
								// unset their spin and decision
								// move to the next player
								// and release the critical section
								...
							}

						}
						// IF the consonant has already been guessed before
						else{
							...
						}

					}
					// IF the player did not respond with a single letter consonant
					else{
						....
					}

				} // END DECISION IFS

			} // END SPIN IF 

		} // END WHILE TRUE
	} // END IF TURN

} // END WHILE gameOn

I removed sections of code related to the presentation of game state to the players as well as general logic for gameplay like if the player had guessed correctly or incorrectly. The focus here is more to present you with the notion that the internal player loop comprises most of the game logic. The reason for this design was so we could have any number of players, and using a semaphores and threads they could interact without having to organize their interactions.

The last piece of important code is our client program. Lets take a look at its main() first:


public static void main(String[] args)
{

	  ////////////////////////////////
	 //    ARGUMENT ACCEPTANCE     //
	////////////////////////////////

	// Argument Protection: avoid any non-whole number strings.
	try
	{
		//- Avoid blank arguments.
		if(args[0].equals("") && args[1].equals("")){
			System.out.println("Usage Error: missing arguments\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
			System.exit(0);
		}
		//- Avoid negative and zero arguments.
		if(Integer.parseInt(args[1]) < 0){
			System.out.println("Usage Error: invalid arguments\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
			System.exit(0);
		}
	}
	catch(NumberFormatException e)
	{
		//- Avoid arguments that are not numeric.
		System.out.println("Usage Error: invalid argument\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
		System.exit(0);
	}
	catch(ArrayIndexOutOfBoundsException e)
	{
		//- Avoid arguments that are not existent.
		System.out.println("Usage Error: missing argument\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
		System.exit(0);
	}

	// Save our host and port arguments
	int port = Integer.parseInt(args[1]);
	String host = args[0];

	// Create our client object
	Client client = new Client(host, port);

	// Game on
	client.listenSocket();
	client.communicate();
}

Very similarly structured to the server program the only real portions of importance are the lines “client.listenSocket();” and “client.communicate();”. Here are the two methods highlighted:


  ///////////////////////////////
 // OPEN SOCKET CONNECTION
///////////////////////////////

public void listenSocket(){
	try{
		// Create our socket
		socket = new Socket(this.host, this.port);

		// Create our communication tools
		out = new PrintWriter(socket.getOutputStream(), true);
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
	}
	catch (UnknownHostException e) {
		System.out.println("Unknown host: " + this.host);
		System.exit(1);
	}
	catch (IOException e) {
		System.out.println("No I/O");
		System.exit(1);
	}

	System.out.println("Welcome, please wait to begin.");
}

  ///////////////////////////////
 // READ or WRITE to SOCKET
///////////////////////////////

public void communicate()
{
	// Create our communication buffers and tools
	Scanner sc = new Scanner(System.in);
	String[] command = new String[2];
	String line = "";
	char inline[] = new char[1000];

	// Keep looking for messages from the server
	while(true)
	{
		// Reset the command recieved from the server to make sure the next one is clear
		command = new String[2];
		command[0] = "";
		command[1] = "";

		// READ FROM THE SERVER
		try{

			// Take a line from the server
			line = in.readLine();

			// Split the line so we know the reponse indetifier and the message
			command = line.split("@");

			// Special terminate command to end our client object
			if(command[0].equals("terminate"))
				System.exit(0);

			// If the command identifier and the message are existant
			if(!command[0].equals("") && !command[1].equals("")){

				// Print the message
				System.out.println(command[1]);
			}
		}
		catch (IOException e){
			System.out.println("Read failed");
			System.exit(1);
		}
		catch (ArrayIndexOutOfBoundsException e){}
		catch (NullPointerException e){}

		// IF response identifier given, ask for a respons to send to the server
		if(command[0].equals("respond")){

			// Ask the user to respond
			String name = sc.nextLine();

			// Send the server the response
			out.println(name);
		}
	}
}

The method listenSocket() only exists to initialize our input and output streams for the network communication. Simple stuff really thanks to Java.

The method communicate() on the other hand contains all the important logic for the client. As you can see the method itself is extremely generic and has no concept of game rules or game messages. In fact its so generic that this method could be used in conjunction with any game server.

That means that our client is coded in a fashion that makes it highly reusable which is a very vital aspect to good software design. In fact the server program itself is very generic and reusable since the entire game logic is within the Player class’ run() method.

This is both very important in that we simply have to swap out Player classes to have entirely different games, making the communication and thread logic reusable and therefore more valuable.

Feel free to download the source and run it for yourself.

Download: SocketWheelOfFortune.zip

To run:

  1. Download source zip.
  2. Decompress and compile Server.Java and Client.java
  3. Run Server with arguments [port], like “java Server 4444″
  4. Run 3 instances of Client with [host] [port] like “java Client localhost 4444″
  5. The server will wait until all three players have joined.