A Simple Diary Package

In order to explain how a package is written and used, let us develop a small example package and work our way through the issues involved in design, implementation, and use. The example package we create uses a simple diary system that allows the storage of appointments, their retrieval, and display (available at https://github.com/jadesoftwarenz/JADE-WP-Packages).

The schema we will be using to export our package is called DiarySchema. It provides two basic classes and their corresponding properties and methods. Note that there is nothing special about a schema that exports a package, and as such, it can be used and tested as a standalone schema before it is converted to a package.

The two major classes will be a DiaryEntry class that will represent an appointment and a Diary class that will hold a collection of these. Any system can have a number of instances of Diary, one for each user for example. The simple system is shown in the following image.

The DiaryEntry class records the time of an appointment with the startTime and duration attributes. The startTime is a TimeStamp containing the exact time an appointment is expected to start. The duration attribute is an Integer representing the expected number of minutes that the appointment will take.

The what attribute (of type String) contains a brief description of the appointment. The index attribute (of type Integer) provides a unique number for each appointment within the diary.

The manual reference myDiary (of type Diary) has an inverse allDiaryEntries reference of type DiaryEntryDict, which is a member key dictionary with keys startTime and index.

The duration method returns a String (for example, "30mins") showing the timespan of an appointment that is used by the display method, which provides further details such as the index, day, time, duration, and description as in:

0 Thu 24 Jul 2003 12:30 30mins Dentist

The Diary class has a single attribute nextIndex (of type Integer) that tracks the next‑highest unique index for a DiaryEntry and two methods. The getAllEntries method returns a String containing all appointments formatted one for each line in the above format.

The makeAppointment method has the following signature.

makeAppointment(startTime         : TimeStamp;
                durationInMinutes : Integer;
                what              : String) updating;

This method creates a new DiaryEntry object, sets all of the attributes of the object, and finally sets the myDiary reference to self to add the new entry to the diary’s allDiaryEntries collection.

The unit tests in the included DiaryTests class provide examples of the DiarySchema in use.

Having tested our schema thoroughly, we now decide to make it available to other users. Without packages, users would need either to load the classes directly into their schema or include DiarySchema somewhere in their schema hierarchy. If they included the complete schema, they would then be able to use these classes in any subschemas below the DiarySchema schema.

However, inserting a schema into an existing schema hierarchy, especially one not designed with this in mind, can be impractical. Firstly, it is not trivial to insert a new schema, and the class names used in the schema may already exist in the schema hierarchy below the level at which you want to insert it. This would preclude the schema from being inserted, as class names must be unique in a schema branch.

A better solution is to create and export a package from DiarySchema and then import it into the schema or schemas where we want to use it. The first step is to decide which classes, properties, and methods we want to export and which should remain hidden. In general, it is good practice to export only those parts of the system that are essential for the package to be useful and to hide all non‑essential details.

In our example, we have decided to hide the manner in which the collection of DiaryEntry objects are stored. The user of the package does not need this information, so we do not need to export the DiaryEntryDict class, the allDiaryEntries collection property, or its inverse myDiary. An advantage of not exporting this information is that if we decide at a later date to change the manner in which these are stored, we can do so without any changes to any code that imports the package. Such a change may require a reorganization of any persistent instances of these classes that the user has created. Another property that we do not need to export is the nextIndex property on Diary.

Having decided on what classes, properties, and methods to export, we can now use the Export Package Definition wizard to define the package. This wizard is available from the Browse menu via the Packages and Export Browser menu items. This brings up the DiarySchema Export Packages Browser form, which enables you to select Add from the Packages menu to display a series of wizard forms, shown in the following images.

The wizard proceeds through the following steps.

  1. The package is named DiaryPackage and an application from the schema is selected.

  2. The classes to be exported, Diary and DiaryEntry, are selected. Note that we have not selected DiaryEntryDict or DiaryTests, and the system classes (for example, Collection), are greyed out, indicating they cannot be exported. Classes that have been selected to be exported are shown in green.

  3. The properties and methods to be exported are selected. Note that the protected members (for example, properties nextIndex, index, and myDiary) are not available for export. In addition, we have chosen not to export the method setMyDiary, which we want to be purely internal to DiarySchema. (The method setMyDiary could not be made protected, as it must be visible to the method makeAppointment on Diary.)

    Any methods or properties can be excluded from a package and will not be visible to the importing schema. The only exception to this is that the create method for a class must be exported if it requires parameters.

  4. When you have selected the features for inclusion in the exported package working set and clicked the Next > button, the Select Interfaces for Package sheet of the Export Package Definition Wizard is then displayed. This fourth sheet enables you to select the interfaces that you want to include in the exported package, if required.

  5. Choose lifetimes and default persistence for the exported classes and access modes for their properties. By default, these are the same as those declared in the exporting schema and can be made only "more restrictive" than their declaration.

    The Export Package Browser, shown in the following image, is then displayed.

When a package has been exported, it can be modified using the Export Package Definition wizard or the Export Package Browser. To open the Export Package Browser for a package, first ensure the Export Packages Browser is open and then double‑click an existing package or select Browse from the Packages menu while an existing package is selected.

The Export Package Browser is similar to a standard Class Browser but limited to the classes, properties, and methods exported in the package. Properties and methods can be added to the package by dragging and dropping them between the normal schema class and package class browsers.

Having defined our exporting package, we can now import it into another schema and use it to create and manipulate diaries. The importing schema need not be a subschema of the exporting schema and would normally have only the RootSchema as a common ancestor in our example schema DiaryTester. We can now import the DiaryPackage into this schema using the Browse menu from the Packages and Import Browser menu items. This displays the DiaryTester Import Packages Browser.

Select the Add menu item from the Packages menu, and then select which exported packages to import.

The Rename Package To text box in the Import Package form enables you to rename the package if the name of the package conflicts with another package that has already been imported.

As we will see, it is not a problem if the names of any of the imported classes conflict with classes already defined in the schema or imported from other packages, as long as the imported classes that have the same names represent different classes. For example, if the same class is imported in two packages, only those packages can be imported into a specific schema.

If you want to allow a circular dependency between packages in the schema hierarchy, check the Include Circular Packages check box. This permits the loading of an incomplete package (for example, Schema1 exports Package1 and imports Package2, while Schema2 exports Package2 and imports Package1).

When you subsequently create a package that would result in circularities, you are prompted to confirm that you want to continue and allow a circular dependency between packages in the schema hierarchy.

Check the Show Details check box to display the package contents.

Having imported the DiaryPackage, opening a normal Class Browser for the schema DiaryTester displays the imported classes along with their properties and methods in green, as shown in the following image. These imported entities cannot be modified.

If the package schema has been encrypted when it was extracted, the source of any exported methods would not be shown in the browser.

The imported classes and methods can now be referenced, as shown in the following JadeScript method example.

createDiary();
vars
    diary : DiaryPackage::Diary;
    today : TimeStamp;
begin
    beginTransaction;
    if app.diary = null then
        create app.diary;
    endif;
    commitTransaction;
    today.setTime('12:30'.Time);
    app.diary.makeAppointment(today, 30, 'Dentist');
    today.setTime('18:00'.Time);
    app.diary.makeAppointment(today, 60, 'Squash');
    write app.diary.getAllEntries();

end;

The reference to the imported class Diary uses the double colon (::) scope operator as in DiaryPackage::Diary. If there is no ambiguity as to which class Diary represents, as in our schema where there is no local Diary class and no other imported Diary class, Diary can be used without the prefix.

When the method call app.diary.getAllEntries is made in this script, a switch is made from the DiaryTester schema into the exporting DiarySchema. Along with this switch is a change in the environmental context of the process. In particular, there is a switch in the meaning of environmental variables such as app, global, and currentSession to those of the schema that exported the package. As a result, while executing the getAllEntries method, the app.diary environment variable of the DiaryTester schema is not available to the getAllEntries method. However, any environment variables on the DiarySchema application class become available. This enables the package developer to use references to these in their code. A common use of this is to access the properties and methods of app in the package, to save context information.

To illustrate this switch of context, suppose we add the following method to class Diary and export it in DiaryPackage.

printAppGlobal();
begin
    write 'App and Global in Diary::printAppGlobal';
    write '    app=' & app.getName & ' global=' & global.getName;
end;

We then run the following JadeScript method in the DiaryTester schema.

showSwitch();
vars
    diary: DiaryPackage::Diary;
begin
    diary := DiaryPackage::Diary.firstInstance;
    write 'App and Global in showSwitch before call';
    write '    app=' & app.getName & ' global=' & global.getName;
    diary.printAppGlobal;
    write 'App and Global in showSwitch after call';
    write '    app=' & app.getName & ' global=' & global.getName;
end;

The output of this JadeScript method is:

App and Global in showSwitch before call
    app=DiaryTester global=GDiaryTester
App and Global in Diary::printAppGlobal
    app=DiarySchema global=GDiarySchema
App and Global in showSwitch after call
    app=DiaryTester global=GdiaryTester

Notice that both app and global have switched from those that apply in the importing schema DiaryTester to those that apply in the exporting schema DiarySchema while executing the exported method printAppGlobal, and are then switched back.