JDataStore is a feature of JBuilder Professional and Enterprise, and the Inprise Application Server.
This chapter contains several simple tutorials that demonstrate basic JDataStore concepts. If you haven't already read the "Introduction", please take a moment to do so before beginning the tutorials.
Table streams can be complete database tables created by the JDBC or DataExpress APIs. They also include cached table data from an external data source such as a database server. Setting the store
property of a StorageDataSet
to the DataStore
creates the cached table data.
File streams can be further broken down into two different categories:
Arbitrary files created with DataStoreConnection.createFileStream( )
. You can write to, seek in, and read from these streams.
Serialized Java objects stored as file streams.
Note: All kinds of streams can be stored in the same JDataStore file.
A case-sensitive name referred to as storeName
in the API identifies each stream. The name can be up to 192 bytes long. The name is stored along with other information about the stream in the JDataStore's internal directory. The forward slash ("/
") is used as a directory separator in the name to provide a hierarchical directory organization. The JDataStore Explorer uses this structure to display the contents of a DataStore
in a tree.
The first part of this chapter covers JDataStore fundamentals using file streams. For information about working with table streams, see "Creating a basic JDBC application using JDataStore", "JDataStore as an embedded database", and "Persisting data in a JDataStore." You may also wish to look at the sample which creates a basic JDBC application using JDataStore in /samples/JDataStore/HelloJDBC/
.
DataStore
is a component that you can program visually. But when you're learning about DataStores
, it might be easier to write simple code examples that demonstrate how a DataStore
works. That's what this chapter has you do.
The classic first exercise for a new language is how to display "Hello, World!" We'll carry that tradition on here. (We'll spare you, however, from performing the classic second exercise, a Fahrenheit to Celsius converter.)
First, create a new project for the dsbasic
package, which you'll use throughout this chapter.
Important: Add the JDataStore library to the project so that you can access the JDataStore classes. If you don't know how to create a project or add a library, see "Creating and managing projects" in Building Applications with JBuilder.
Hello.java
, and type in this code:
// Hello.java package dsbasic; import com.borland.datastore.*; public class Hello { public static void main( String[] args ) { DataStore store = new DataStore(); try { store.setFileName( "Basic.jds" ); if ( !new java.io.File( store.getFileName() ).exists() ) { store.create(); } else { store.open(); } store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } }After declaring its package, this class imports all the classes in the
com.borland.datastore
package. That package contains most of the public JDataStore classes. (The rest of the public JDataStore classes are in the com.borland.datastore.jdbc
package, which is needed only for JDBC access. It contains the JDBC driver class, and classes used to implement a JDataStore Server. These classes are covered in "JDataStore as an embedded database" and "Multi-user and remote access to JDataStores.") You can also access JDataStore through DataExpress components (packages under com.borland.dx
). In this example, these classes are referenced explicitly so that you can see where each class comes from.
DataStore
object is created in the main()
method of Hello.java
. This object represents a physical JDataStore file and it contains properties and methods that represent its structure and configuration.
Next, the name "Basic.jds" is assigned to the DataStore
object's fileName
property. It contains the default file extension ".jds" in lowercase. If the file name doesn't end with the default extension, the extension is appended to the file name when the property is set.
You can't create the JDataStore if a file with that name already exists. If the file doesn't exist, the create()
method creates it. If the method fails for any reason (for example, there's no room on the disk, or someone just created the file in the nanoseconds between this statement and the last), it throws an exception. If the method succeeds, you have an open connection to a new JDataStore file.
DSX: See "Creating a new JDataStore file." When creating the file, you can also specify options like block size and whether the JDataStore is transactional.
open()
method. The open()
method is actually a method of the DataStore
class' superclass,
DataStoreConnection
, which contains properties and methods for accessing the contents of a JDataStore. (The fileName
property is also a property of DataStoreConnection
, which means that you can and often do access a JDataStore without a DataStore
object, as you'll see shortly.) Because DataStore
is a subclass of DataStoreConnection
, it has its own built-in connection, which is suitable for simple applications like this. (Note that DataStore
can create a new JDataStore file, but DataStoreConnection
cannot.)
But the excitement is short-lived. Immediately after opening a connection to the JDataStore file, creating the file in the process if necessary, that connection closes with the close()
method. The close()
method is also inherited from DataStoreConnection
. Because there was only one built-in connection, when all the connections to the JDataStore are closed, the JDataStore file itself shuts down.
You must close any connections that you open before you exit your application (or call the DataStore
.shutdown()
method, which closes all connections). Opening a connection starts a daemon thread that continues to run and prevents your application from terminating properly. If you don't close the connections, your application will hang on exit.
DataSetException
, or more specifically, one of its subclasses,
DataStoreException
. Most of these exceptions are of the fatal "should never happen" or "don't do that" variety. For example, you can't set the fileName
property if the connection is already open. You can't create the JDataStore file if one already exists. You can't open a connection if the named file isn't really a JDataStore file. You might get an IO exception when writing data when closing a connection.
Therefore, almost all JDataStore code is inside a try
block. In this case, if an exception is thrown, a stack trace prints.
Basic.jds
. If you then run it a second time, it does even less--just opening and closing a connection. Before you go further, you should delete the file.
There is no special function for deleting a JDataStore file. You can use the java.io.File.delete()
method or anything else that accomplishes the task. As an aside example, if you always want to create a new JDataStore file, you write something like this code fragment:
// store is DataStore with fileName property set java.io.File storeFile = new java.io.File( store.getFileName() ); if ( storeFile.exists() ) { storeFile.delete(); } store.create();
If the JDataStore file is transactional, it is accompanied by transaction log files, which must also be deleted. For more information on transaction log files, see "Transaction log files".
DSX: See "Deleting the JDataStore file". The JDataStore Explorer automatically deletes any associated transaction log files.
if
block in the main()
method:
if ( !new java.io.File( store.getFileName() ).exists() ) { store.create(); try { store.writeObject( "hello", "Hello, JDataStore! It's " + new java.util.Date() ); } catch ( java.io.IOException ioe ) { ioe.printStackTrace(); } } else {
The writeObject()
method attempts to store a Java object as a file stream in the JDataStore using Java serialization. (Note that you can also store objects in a table.) The object to be stored must implement the java.io.Serializable
interface. A java.io.IOException
(more specifically, a java.io.NotSerializableException
) is thrown if it doesn't. Another reason for the exception would be if the write failed (for example, you ran out of disk space).
The first parameter of writeObject()
specifies the storeName
, the name that identifies the object in the JDataStore. The name is case-sensitive. The second parameter is the object to store. In this case, it is a string with a greeting and the current date and time. The java.lang.String
class implements java.io.Serializable
, so the string can be stored with writeObject
.
else
block in the main()
method:
} else { store.open(); try { String s = (String) store.readObject( "hello" ); System.out.println( s ); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } catch ( java.lang.ClassNotFoundException cnfe ) { cnfe.printStackTrace(); } catch ( java.io.IOException ioe ) { ioe.printStackTrace(); } }
The readObject()
method attempts to retrieve the named object from the JDataStore. Like writeObject()
, it can throw an IOException
for reasons like disk failure. It also can't reconstitute the stored object without the object's class. If that class is not in the classpath, readObject()
throws a java.lang.ClassNotFoundException
.
If the named object can't be found, a DataStoreException
with the error code STORE_NOT_FOUND
is thrown. DataStoreException
is a subclass of DataSetException
. It's important to catch that exception here, even though there's another catch
at the bottom of the method, because jumping there would bypass the call to close()
the JDataStore connection. (The code is structured in this somewhat awkward way to teach certain principles.)
Because readObject()
returns a java.lang.Object
, you almost always cast the return value to the expected data type. (If the object isn't actually of that expected type, you get a java.lang.ClassCastException
.) Here, it's more of a formality, because the System.out.println
method can take a generic Object
reference.
Hello.java
. The first time it runs, it creates the JDataStore file and stores the greeting string. When you run it again, the greeting with the date and time displays in the console.
For the simple persistent storage of objects, the JDataStore has a number of advantages over using the JDK classes in the java.io
package:
FileOutputStream
, ObjectOutputStream
, FileInputStream
, ObjectInputStream
).
You can keep all your objects in a single file and easily access them with a logical name instead of streaming all your objects to the same file.
With a single file, you can't accidentally lose an object or two as you might with separate files. You might also use less storage space, because separate files can waste a lot of space because of how disk clusters are allocated. The default block size in a JDataStore file is small (4KB).
Because you're not at the mercy of the host file system, your application is more portable. For example, different operating systems have different allowable characters for names. Some systems are case-sensitive, while others are not. Naming rules inside the JDataStore are consistent on all platforms.
It provides an encryptable file system.
An internal directory system is of little use if you don't have a way to get the contents of the directory.
DataStoreConnection
.openDirectory()
method returns the contents of the JDataStore in a searchable structure. But first, add the following program, AddObjects.java
, to the project and run it to add a few more objects to the JDataStore:
// AddObjects.java package dsbasic; import com.borland.datastore.*; public class AddObjects { public static void main( String[] args ) { DataStoreConnection store = new DataStoreConnection(); int[] intArray = { 5, 7, 9 }; java.util.Date date = new java.util.Date(); java.util.Properties properties = new java.util.Properties(); properties.setProperty( "a property", "a value" ); try { store.setFileName( "Basic.jds" ); store.open(); store.writeObject( "add/create-time", date ); store.writeObject( "add/values", properties ); store.writeObject( "add/array of ints", intArray ); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } catch ( java.io.IOException ioe ) { ioe.printStackTrace(); } finally { try { store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } }
The program does things slightly differently than Hello.java
. First, it uses a DataStoreConnection
object instead of a DataStore
to access the JDataStore file, but it's used in the same way. You set the fileName
property, open()
the connection, use the writeObject()
method to store objects, and close()
the connection.
The location of the close()
method call is another difference. Because you always want to call close()
no matter what happens in the main body of the method, it's placed after the catch
blocks inside a finally
block. This way, the connection always closes, even if there is an unhandled error. The close()
method is safe to call even if the connection never opened. In that case, close()
does nothing.
This time, three objects are written to the JDataStore: an array of integers, a Date
object (not a Date
object converted into a string), and a hashtable. They are named so that they will be in a directory named "add." The forward slash (/) is the directory separator character. One of the names contains spaces, which is perfectly valid.
Dir.java
:
// Dir.java package dsbasic; import com.borland.datastore.*; public class Dir { public static void print( String storeFileName ) { DataStoreConnection store = new DataStoreConnection(); com.borland.dx.dataset.StorageDataSet storeDir; try { store.setFileName( storeFileName ); store.open(); storeDir = store.openDirectory(); while ( storeDir.inBounds() ) { System.out.println( storeDir.getString( DataStore.DIR_STORE_NAME ) ); storeDir.next(); } store.closeDirectory(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } finally { try { store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } public static void main( String[] args ) { if ( args.length > 0 ) { print( args[0] ); } } }This class needs a command-line argument, the name of a JDataStore file, which is passed to its
print()
method. The print()
method accesses that JDataStore using code similar to what you've seen before.
Dir.java
defines a DataStoreConnection
to access the JDataStore and also declares a StorageDataSet
. After opening a connection to the JDataStore, the program calls the
openDirectory()
method of the DataStoreConnection
to get the contents of the JDataStore's directory. The directory of a JDataStore is represented by a table.
DSX: See "Viewing JDataStore file information."
You can reference the columns by name or number. There are
constants defined as DataStore
class variables for each of the column names. The best way to reference these columns is to use these constants. They provide compile-time checking to ensure that you are referencing a valid column. Constants with names that end with _STATE
exist for the different values for the State column. There are also constants for the different values and bit masks for the Type column with names that end with _STREAM
.
java.util.Date(long)
.
As with many file systems, when you delete something in a JDataStore, the space it occupied is marked as available, but the contents and the directory entry that points to it are not wiped clean. This means maybe you can undelete something. For more details, see "Deleting and undeleting streams."
The Type column indicates whether a stream is a file or table stream, but there are also many internal table stream subtypes (for things like indexes and aggregates). These internal streams are marked with the HIDDEN_STREAM
bit to indicate that they should not be displayed. Of course, when you're reading the directory, you can decide whether they should be hidden or visible.
These internal streams have the same StoreName as the table stream with which they're associated. This means that the StoreName alone doesn't always uniquely identify each
stream when they interact with the JDataStore at a low level.Often some internal stream types have multiple instances. Therefore, the ID for each stream must guarantee uniqueness at a low level. But the StoreName is unique enough for the storeName
parameter used at the API level. For example, when you delete a table stream, all the streams with that StoreName are deleted.
DataSetView
to use a different sort order.)
next()
and inBounds()
methods to navigate through each entry in the directory. Use the appropriate get<XXX>()
method to read the desired information for each stream.
You can't write to the JDataStore directory because it is read-only.
To run Dir.java
, set the runtime parameters in the Project Properties dialog box to the JDataStore file to check, which in this case, is Basic.jds
. When it runs, a loop goes through the directory, listing the name of every stream, such as this:
add/array of ints add/create-time add/values hello
You can include a lot more information in the directory listing. The most difficult part is making the formatting decisions for the various bits of information available in all the columns of the JDataStore directory. To display whether the stream is a table or file stream, for example, add the boldfaced statements to the beginning of the loop:
while ( storeDir.inBounds() ) { short dirVal = storeDir.getShort( DataStore.DIR_TYPE ); if ( (dirVal & DataStore.TABLE_STREAM) != 0 ) { System.out.print( "T" ); } else if ( (dirVal & DataStore.FILE_STREAM) != 0 ) { System.out.print( "F" ); } else { System.out.print( "?" ); } System.out.print( " " ); System.out.println( storeDir.getString( DataStore.DIR_STORE_NAME ) ); storeDir.next(); }
That addition changes the output to this:
F add/array of ints F add/create-time F add/values F hello
The output indicates that all the serialized objects are indeed file streams.
DataStoreConnection.closeDirectory()
method. Most JDataStore operations modify the directory in some way. If the directory is open, it must be notified, which slows down your application.
If you try to access the directory StorageDataSet
when the directory is closed, you get a DataSetException
with the error code DATASET_NOT_OPEN
.
DataStoreConnection
provides two methods for checking if a stream exists, without having to open the directory. The
tableExists()
method checks for table streams and the
fileExists()
method checks for file streams. Both methods take a storeName
parameter and they ignore streams that are deleted. They return true
if there is an active stream of the corresponding type with that name in the JDataStore, or false
otherwise. Remember that stream names are case-sensitive and that you can't have a table stream and a file stream with the same name.
For example, suppose you ran the following code fragment against Basic.jds
as it is at this point in the tutorial:
store.tableExists( "hello" )
It returns false
because although there is a stream named "hello", it's a file stream, not a table stream. The same result occurs with this:
store.fileExists( "Hello" )
This time the name doesn't match case. Here the name and type match:
store.fileExists( "hello" )
com.borland.datastore.FileStream
object. Although FileStream
is a subclass of java.io.InputStream
, it has a method for writing to the stream as well so the same object can be used for both read and write access. It also provides random access with a seek()
method. Because FileStream
is a subclass of InputStream
, it's easy to use streams stored in the JDataStore in generic situations that expect an input stream. You'll probably read a stream more often than you write one.
DSX: See "Importing files."
ImportFile.java
, does this for you. Add it to the project.
// ImportFile.java package dsbasic; import com.borland.datastore.*; public class ImportFile { private static final String DATA = "/data"; private static final String LAST_MOD = "/modified"; public static void read( String storeFileName, String fileToImport ) { read( storeFileName, fileToImport, fileToImport ); } public static void read( String storeFileName, String fileToImport, String streamName ) { DataStoreConnection store = new DataStoreConnection(); try { store.setFileName( storeFileName ); store.open(); FileStream fs = store.createFileStream( streamName + DATA ); byte[] buffer = new byte[ 4 * store.getDataStore().getBlockSize() * 1024 ]; java.io.File file = new java.io.File( fileToImport ); java.io.FileInputStream fis = new java.io.FileInputStream( file ); int bytesRead; while ( (bytesRead = fis.read( buffer )) != -1 ) { fs.write( buffer, 0, bytesRead ); } fs.close(); fis.close(); store.writeObject( streamName + LAST_MOD, new Long( file.lastModified() ) ); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } catch ( java.io.FileNotFoundException fnfe ) { fnfe.printStackTrace(); } catch ( java.io.IOException ioe ) { ioe.printStackTrace(); } finally { try { store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } public static void main( String[] args ) { if ( args.length == 2 ) { read( args[0], args[1] ); } else if ( args.length >= 3 ) { read( args[0], args[1], args[2] ); } } }The program takes as parameters the name of a JDataStore file, the name of the file to import, and an optional stream name. If you don't specify a file stream name, the file name is used. The
main()
method calls the appropriate form of the read()
method, because the two-argument read()
method calls the three-argument read()
method.
When the file is imported, the date it was last modified is recorded with it. The "/modified" suffix appends to the stream name for this date, while the "/data" suffix appends to the stream name to contain the data from the file. These suffixes are defined as class variables.
The read()
method then begins by opening a connection to the JDataStore file with a DataStoreConnection
object.
createFileStream()
and its only parameter is the storeName
of the stream to create.
If there is already a file stream with that name, even if it's actually a serialized object, it will be lost without warning. You might want to check if such a file stream exists with the fileExists()
method first (ImportFile.java
does not). If there is a table stream with that name, createFileStream()
throws a DataStoreException
with the error code
DATASET_EXISTS
, because you can't have a table stream and a file stream with the same name.
When createFileStream
is successful, it returns a
FileStream
object that represents the new, empty file stream.
The JDataStore's block size is stored in the DataStore
object's blockSize
property. Whenever you use a DataStoreConnection
to access a JDataStore, it automatically creates an instance of DataStore
. Other DataStoreConnection
objects in the same process that connect to the same JDataStore share that DataStore
object. (Access to a JDataStore file is exclusive to a single process.
Multi-user access is provided through a single server process.) The DataStoreConnection
has a read-only property named dataStore
that contains a reference to the connected DataStore
object.
The FileStream
object writes an array of bytes. The array is declared in this statement:
byte[] buffer = new byte[ 4 * store.getDataStore().getBlockSize() * 1024 ];The
getDataStore()
method gets the reference to the DataStore
object, and from that the getBlockSize()
method gets the blockSize
property. The property value is in kilobytes so it is multiplied by 1024. The resulting block size is multiplied by four, the arbitrarily-chosen number of blocks to read in each chunk.
FileStream
object's write()
method takes an array of bytes such as a java.io.OutputStream
, although the only form of the method is the one that also specifies the starting offset and length.
The java.io.FileInputStream
object reads from a file into an array of bytes. It returns the number of bytes read, or -1 if the end-of-file is reached. In the loop, the number of bytes read is checked for the end-of-file value. If it's not the end-of-file, the number of bytes read are written, starting with the first byte in the array. For every iteration of the loop except the last, the entire array is filled by reading and writing into the FileStream
. The last iteration probably won't fill the entire array.
FileStream
object uses the close()
method (as does the FileInputStream
).
After the file stream is closed, the last-modified date is written using a java.lang.Long
object to encapsulate the primitive long
value. (You cannot save primitives with serialization.)
To test ImportFile.java
, try importing some source code files into Basic.jds
.
openFileStream()
method to open an existing file stream by name. Like createFileStream()
, it returns a FileStream
object at the beginning of the stream. You can then go to any position in the stream with the
seek()
method, write to the stream, and read from it with the read()
method. FileStream
also supports
InputStream
marking with the
mark()
and
reset()
methods.
The PrintFile.java
program demonstrates opening, seeking, and reading. Add it to the project.
// PrintFile.java package dsbasic; import com.borland.datastore.*; public class PrintFile { private static final String DATA = "/data"; private static final String LAST_MOD = "/modified"; public static void printBackwards( String storeFileName, String streamName ) { DataStoreConnection store = new DataStoreConnection(); try { store.setFileName( storeFileName ); store.open(); FileStream fs = store.openFileStream( streamName + DATA ); int streamPos = fs.available(); while ( --streamPos >= 0 ) { fs.seek( streamPos ); System.out.print( (char) fs.read() ); } fs.close(); System.out.println( "Last modified: " + new java.util.Date( ((Long) store.readObject( streamName + LAST_MOD )).longValue() ) ); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } catch ( java.io.IOException ioe ) { ioe.printStackTrace(); } catch ( java.lang.ClassNotFoundException cnfe ) { cnfe.printStackTrace(); } finally { try { store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } public static void main( String[] args ) { if ( args.length == 2 ) { printBackwards( args[0], args[1] ); } } }To demonstrate random access with the
seek
method (and to make things slightly more interesting), this program prints a file stream backwards. It determines the length of the file stream by calling the FileStream
's available()
method and uses it as a file pointer. When reading from the file, the program moves the file pointer forward. The position of the file pointer decrements and is set for each byte read in the loop. There are two forms of the
read()
method. The first reads into a
byte array (the same form of the method used by the FileInputStream
in ImportFile.java
). The second returns a single byte. Here the single-byte form is used. Each byte is cast into a character to be printed.
Now that you've learned about creating and manipulating file streams in a JDataStore, it's time to teach you the basics of creating a JDBC application using JDataStore. For more detailed information about creating JDBC applications using JDataStore, see the chapter on "JDataStore as an embedded database".
We'll start by creating a new file in the dsbasic package called HelloTX.java
. The code for this is very similar to the Hello.java
file you created earlier. The differences are shown in boldface:
// HelloTX.java package dsbasic; import com.borland.datastore.*; public class HelloTX { public static void main( String[] args ) { DataStore store = new DataStore(); try { store.setFileName( "BasicTX.jds"); store.setUserName("CreateTX"); store.setTXManager(new TxManager()); if ( !new java.io.File( store.getFileName() ).exists() ) { store.create(); } else { store.open(); } store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } }
The most important difference here is that a TxManager
is instantiated and assigned to be the transaction manager for the JDataStore. A JDBC application requires a transactional JDataStore, so a transaction manager is necessary. To create (or open) a transactional JDataStore, you must also set the userName
property. If there is no name in particular that you find appropriate, you can set it to a dummy name.
The next step is to write some code that connects to the DataStore
. Add a file called HelloJDBC.java
to the project. Type the following code into the new file:
//HelloJDBC.java package dsbasic; import java.sql.*; public class HelloJDBC { public HelloJDBC() { } static void main(String args[]) { // Both the remote and local JDatastore drivers use the same // driver string: String DRIVER = "com.borland.datastore.jdbc.DataStoreDriver"; // Use this string for the local driver: String URL = "jdbc:borland:dslocal:"; // Use this string for the remote driver (and start JDataStore Server): // String URL = "jdbc:borland:dsremote://localhost/"; String FILE = "BasicTX.jds"; boolean c_open=false; Connection con = null; try { Class.forName(DRIVER); con = DriverManager.getConnection(URL + FILE, "user", ""); c_open = true; } catch(Exception e) { System.out.println(e); } // This way the connection will be closed even when exceptions are thrown // earlier. This is important, because you may have trouble reopening // a JDatastore file after leaving a connection to it open. try { if(c_open) con.close(); } catch(Exception e3) { System.out.println(e3.toString()); } } }
Note the boldface lines of code in this program. They are the most important ones to note. First, the driver string for the JDataStore JDBC driver is specified. This string is always the same for both the local and remote JDBC drivers. Next, the URL string for connecting to a local JDataStore is shown. For your information, the code also shows the remote string, but this is commented out. The last two boldface lines are common to many JDBC applications, and they're where we actually connect to the JDataStore.
Once you've connected to the JDataStore, you'll probably want to add and manipulate some data. We'll show you how to do that next. We won't spend a lot of time on it here, just enough to let you know that you have connected to the JDataStore, and can add, manipulate, print, and delete data. Add the following boldfaced lines to the code as shown:
package dsbasic; import java.sql.*; public class HelloJDBC { public HelloJDBC() { } public static String formatResultSet(ResultSet rs) { // This method formats the result set for printing. try { ResultSetMetaData rsmd = rs.getMetaData(); int numberOfColumns = rsmd.getColumnCount(); StringBuffer ret = new StringBuffer(500); for (int i = 1; i <= numberOfColumns; i++) { String columnName = rsmd.getColumnName(i); ret.append(columnName + "," ); } ret.append("\n"); while (rs.next()) { for (int i = 1; i <= numberOfColumns; i++) ret.append(rs.getString(i) + "," ); ret.append("\n"); } return(ret.toString()); } catch(Exception e) { return e.toString(); } } static void main(String args[]) { // Both the remote and local JDatastore drivers use the // same driver string: String DRIVER = "com.borland.datastore.jdbc.DataStoreDriver"; // Use this string for the local driver: String URL = "jdbc:borland:dslocal:"; // Use this string for the remote driver (and start JDataStore Server): // String URL = "jdbc:borland:dsremote://localhost/"; String FILE = "BasicTX.jds"; boolean s_open=false, c_open=false; Statement stmt = null; Connection con = null; try { Class.forName(DRIVER); con = DriverManager.getConnection(URL + FILE, "user", ""); c_open = true; stmt = con.createStatement(); s_open = true; // The following line creates a table in the JDataStore. stmt.executeUpdate("create table HelloJDBC" + "(COLOR varchar(15), " + " NUMBER int, " + " PRICE float)"); // Values are inserted into the table with // the next three statements. stmt.executeUpdate("insert into HelloJDBC values('Red', 1, 7.99)"); stmt.executeUpdate("insert into HelloJDBC values('Blue', 2, 8.99)"); stmt.executeUpdate("insert into HelloJDBC values('Green', 3, 9.99)"); // Now we query the table ResultSet rs = stmt.executeQuery("select * from HelloJDBC"); // Call to formatResultSet() to format the // printed output. System.out.println(formatResultSet(rs)); // The next line deletes the table. stmt.executeUpdate("drop table HelloJDBC"); } catch(Exception e) { System.out.println(e); } try { // Attempt to clean up by calling the // java.sql.Statement.close() method. if(s_open) stmt.close(); } catch(Exception e2){ System.out.println(e2.toString()); } // This way the connection will be closed even when exceptions are thrown // earlier. This is important, because you may have trouble reopening // a JDatastore file after leaving a connection to it open. try { if(c_open) con.close(); } catch(Exception e3) { System.out.println(e3.toString()); } } }
In the preceding example, the code added to the main()
method creates a table and inserts rows in the table. Then it calls the formatResultSet()
method and prints the results. Next, it deletes the table from the JDataStore. Finally, it attempts to clean up by calling the close()
method of the java.sql.Statement
object.
DataStoreConnection
class'
copyStreams()
method makes a new copy of one or more streams in the same JDataStore or it copies the streams to a different JDataStore. If it encounters an error in an original stream, it attempts to correct that error in the copy. Also, you can use copyStreams()
to upgrade an older
JDataStore file into the current format.
copyStreams
method takes six parameters, as listed in this table:
Each of the options
reverses the default behavior of copyStreams
. The default behavior
If copyStreams()
stops because either of the last two conditions occur, it throws a DataSetException
. Status messages for each stream that is copied are output to the designated PrintStream
.
DSX: The JDataStore Explorer provides a UI for copying streams to a new JDataStore file with these parameters. See "Copying JDataStore streams."
copyStreams()
method is unaware of a directory structure. It simply treats names as strings. You must use the forward slash when necessary to impose structure.
The first two parameters, sourcePrefix
and sourcePattern
, determine which streams are copied. sourcePrefix
is used in combination with the destPrefix
parameter to rename a stream when it is copied; that is, to change the prefix (the beginning) of the storeName
of the resulting copy of the stream.
If you specify a sourcePrefix
, the stream name must start with that string. It's usually used to specify the name of a directory ending with a forward slash. The destPrefix
is then set to a different directory name also ending with a forward slash. The sourcePrefix
is stripped from the name, and the destPrefix
is prepended to the name of the copy. For example, suppose you have the stream named "add/create-time" and you want to create a copy named "tested/create-time". The effect is to make a copy in a different directory. You would set sourcePrefix
to "add/" and destPrefix
to "tested/".
Although the prefix parameters are usually used for directories, you can rename streams in other ways. For example, you can rename "hello" to "jello" by specifying "h" and "j" for the sourcePrefix
and destPrefix
respectively. Or you can change "three/levels/deep" to "not-a-peep" by specifying "three/levels/d" and "not-a-p". The effect is to move a stream up to the root directory of the JDataStore. You can also do the reverse by making the destPrefix
longer (with more directory levels) than the sourcePrefix
. For example, by leaving the sourcePrefix
blank, but specifying a destPrefix
that ends with a forward slash, all the streams from the original JDataStore file are placed under a directory in the destination JDataStore.
If you're not renaming the copy of the stream, there's no reason to use either prefix parameter, so you should set both of them to an empty string or null
. Note that if you're making a copy of a stream in the same JDataStore file, you must rename the copy.
The sourcePattern
parameter is matched against everything after the sourcePrefix
, using the standard wildcard characters "*" (for zero or more characters) and "?" (for a single character). If the sourcePrefix
is empty, that means that the pattern is matched against the entire string. If you want to copy all the streams in a directory, you can put the directory name in the sourcePattern
, followed by a forward slash, and leave the sourcePrefix
empty. For example, if you want to copy everything in the "add" directory, you want to copy everything that starts with "add/", so the sourcePattern
would be "add/*". That includes everything in subdirectories, because the sourcePattern
matches the remainder of the string. (There is no direct way to prevent the copying of streams in subdirectories.)
The sourcePattern
is matched against names of active streams only. copyStreams()
doesn't copy deleted streams.
Dup.java
, to make a backup copy of a JDataStore file or upgrade an older file:
// Dup.java package dsbasic; import com.borland.datastore.*; public class Dup { public static void copy( String sourceFile, String destFile ) { DataStoreConnection store1 = new DataStoreConnection(); DataStore store2 = new DataStore(); try { store1.setFileName( sourceFile ); store2.setFileName( destFile ); if ( !new java.io.File( store2.getFileName() ).exists() ) { store2.create(); } else { store2.open(); } store1.open(); store1.copyStreams( "", // From root directory "*", // Every stream store2, "", // To root directory DataStore.COPY_IGNORE_ERRORS, System.out ); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } finally { try { store1.close(); store2.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } public static void main( String[] args ) { if ( args.length == 2 ) { copy( args[0], args[1] ); } } }This program copies the contents of one store into another. It uses a
DataStoreConnection
object to open the source JDataStore. It copies the contents to a DataStore
object so that the JDataStore file can be created if it doesn't already exist.
For the copyStreams
method, the sourcePrefix
and destPrefix
are empty strings, and the sourcePattern
is just "*", which copies everything without renaming anything. The program ignores unrecoverable errors and displays status messages in the console.
With this program you can combine the contents of more than one JDataStore file into a single file, as long as the stream names are different (COPY_OVERWRITE
is not specified as an option).
DataStoreConnection.deleteStream()
method takes the name of the stream to delete. For a file stream, the individual stream is deleted. For a table stream, the main stream and all its support streams are deleted.
Deleting a stream doesn't actually overwrite or clear the stream contents. Like most file systems, the space used by the stream is marked as available, and the directory entry that points to that space is marked as deleted. The time the stream was deleted is recorded. Over time, new stream contents might overwrite the space that was formerly occupied by the deleted stream, making the content of the deleted stream unrecoverable.
DSX: See "Deleting streams".
The DataStoreConnection.undeleteStream()
method takes such a row as a parameter. You can pass the directory dataset itself. The current row in the directory dataset is used as the row to undelete.
Note that you can create a new stream with the name of a deleted stream. You can't undelete that stream while its name is being used by an active stream.
DSX: See "Undeleting streams."
DeleteTest.java
, demonstrates both deletion and undeletion.
// DeleteTest.java package dsbasic; import com.borland.datastore.*; public class DeleteTest { public static void main( String[] args ) { DataStoreConnection store = new DataStoreConnection(); com.borland.dx.dataset.StorageDataSet storeDir; com.borland.dx.dataset.DataRow locateRow, dirEntry; String storeFileName = "Basic.jds"; String fileToDelete = "add/create-time"; try { store.setFileName( storeFileName ); store.open(); storeDir = store.openDirectory(); locateRow = new com.borland.dx.dataset.DataRow( storeDir, new String[] { DataStore.DIR_STATE, DataStore.DIR_STORE_NAME } ); locateRow.setShort( DataStore.DIR_STATE, DataStore.ACTIVE_STATE ); locateRow.setString( DataStore.DIR_STORE_NAME, fileToDelete ); if ( storeDir.locate( locateRow, com.borland.dx.dataset.Locate.FIRST ) ) { System.out.println( "Deleting " + fileToDelete ); dirEntry = new com.borland.dx.dataset.DataRow( storeDir ); storeDir.copyTo( dirEntry ); store.closeDirectory(); System.out.println( "Before delete, fileExists: " + store.fileExists( fileToDelete ) ); store.deleteStream( fileToDelete ); System.out.println( "After delete, fileExists: " + store.fileExists( fileToDelete ) ); store.undeleteStream( dirEntry ); System.out.println( "After undelete, fileExists: " + store.fileExists( fileToDelete ) ); } else { System.out.println( fileToDelete + " not found or already deleted" ); store.closeDirectory(); } } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } finally { try { store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } }In this program, the name of the JDataStore file and the stream to be deleted are hard-coded, which you would seldom do. The stream is "add/create-time", which was added to
Basic.jds
in the AddObjects.java
demonstration program. It's a file stream primarily because the fileExists()
method is used to check whether the deletion and undeletion worked.
Note:Usually you would probably locate the directory entry for the stream after it has been deleted and use the directory dataset to undelete the stream. It's done differently here to demonstrate individual directory rows, which are explained shortly.
To locate the row, a new com.borland.dx.dataset.DataRow
is instantiated from the directory dataset, specifying the two columns that are used in the search: the State and StoreName. The program then attempts to locate the directory entry for the specified stream, which must be active. Finding the row not only positions the directory at the desired entry, but it also indicates that the stream exists and is active so that the program can proceed to the next step.
undeleteStream()
, the current row is used. But because of the way the JDataStore directory is sorted (as explained in "Directory sort order"), when a stream is deleted, its directory entry probably "flies away" to its new position at the bottom of the directory as the most recently deleted stream. The current row is then referencing something else (probably the next stream alphabetically). To undelete the same stream, you could either attempt to relocate the directory entry for the now-deleted stream, or you can copy the directory data for the stream into a separate directory row before you delete.
Using an individual directory row has a few advantages. Unlike the live JDataStore directory dataset, an individual row is a static copy. It's smaller. After making the copy, you can close the directory dataset to make operations faster. (For this simple demonstration, the overhead for creating the individual row probably outweighs any performance benefit.) You can make static copies of as many directory entries as you want, and manage them any way you want.
To create the individual directory row, another DataRow
is instantiated from the directory dataset (so that it has the same structure), and the copyTo()
method copies the data from the current row. And just to prove that it really works, the JDataStore directory is closed.
The file stream is then deleted by name using the plain name string defined at the beginning of the method. Finally, the stream is undeleted using the individual directory entry.
copyStreams()
. Only active streams are copied, which results in a packed version of the file.
DSX: See "Packing the JDataStore file."