Filter source examples

alpha version, October 2000

<<<note that the API has bees improved somewhat to allow for even simpler filter programming. Do please study the examples even though, since the principles has never been changed, only some details. We shall make the documentation upgrade soon>>>

To implement a new filter, you have to prepare one or two new classes, placed in one DLL: an optional filter class (inherited from the CXCFilter), and a conversion class (inherited from the CXCConversion).

There are three examples here:


The simplest possible filter

For starters, let's make a simplest filter which would convert a plain file to a plain file (or any other stream to any stream: the very same filter could be used, say, to translate the text clipboard contents!). There will be just one input type and one output type; thence, we would subclass only one class, the CXCConversion, and just one method there:

@interface TestConversion:CXCConversion
-void doConvert(CXCProgress *progress);
@end

The filter will replace any comma in the data by a semicolon. Besides, it adds a comment to the head of the output data, containing the original file name. The implementation is straighforward: the input is read line by line (using the CXCStreamData API), the conversion is performed on each line, and the result is written out.

@implementation TestConversion
-void doConvert
{
    CXCStreamData *inp=(CXCStreamData*)_input,*out=(CXCStreamData*)_output;
    CXString *line;

    // prepare the progress indicator
    [_progress start:[inp size]];
    // prepend a comment with some information of the filter used
    [out writeFormat:@"// translated from <%@> by \"%@\"\n\n",[inp filename],[[self currentFilter] name]];
    // let us start to convert!
    while ((line=[inp readLine])!=nil) {
        // it is recommended to set the pool, otherwise *ALL* the allocated objects
        // would be released only after the *WHOLE* conversion is finished:
        CXAutoreleasePool *pool=[CXAutoreleasePool pool];

        [_progress step:[s length]];
        // perform the replacement (see the CXString and CXArray classes)
        line=[[line 
componentsSeparatedByString:@","] componentsJoinedByString:@";"];
        // write out the result
        [out writeString:s];
        [out writeString:@"\n"]; // the delimiter was NOT returned from the readLine!
        // release all the automatically allocated objects
        [pool release];
    }
}
@end

The code should be quite clear, so just a few short remarks:

(*) There is a small bug still--objects, which are created as a side effect of the [inp readLine], are released only when the conversion is done. It would be better to perform the reading inside the CXAutoreleasePool harness.

Finally, let us see how the filter will be created in the DLL ordinal 1 function:

id ordinal0neExport(void)
{
    CXCDataType *idt=[CXCDataType dataTypeWith:
        @"Plain t
ext", // a human readable name
        KDataTypePriorityNormal, // normal qualification (this will change, see CXCDataType class)
        @"text/as
cii", // the MIME type
        [CXArrayClass arrayWithObjects:@"txt",@"ascii",@"TXT",nil]]; // extensions
    return [CXCFilter filterWith:
        idt,idt, // input and output data types
        TestConversionClass, // the only conversion class
        @"Plain Text Filter", // the filter name...
        @"1.00", // ...version...
        @"OCSoftware", // ...creator...
        @"A Stupid Test Filter, good for nothing"]; // ...and description
}

Note here that the extensions in the data type are case sensitive. There is a special API to specify case insensitive extensions as well.


An encoding filter with dialogue

Now, let's extend the filter from the previous example a bit: let's add a detail dialogue to set the searched and replaced strings, and let's allow the source and target encodings to be specified by the user. Still we will subclass just the CXCConversion, but this time we need to reimplement some more methods; besides, we will need a few property variables:

@interface DialogueConversion:CXCConversion
{
    CXString *from,*to; // the strings to be found and replaced
}
-void begin; // to set the default values
-void finish; // to release owned values
-CXArray *details;
-CXString *validateDetails;
-void doConvert(CXCProgress *progress);
@end

The from and to variables will simply contain the string to be searched for (from) and the one to be replaced instead the former (to). We do not need any variables for the input and output encodings, for they will be stored in the filter defaults (it is a reasonable presumption that the user will generally convert the next file using the same input and output encodings as for the previous one).

Let's see the implementation. For starters, the begin method just sets the default values for the properties (actually, we could set these just as well in the beginning of the details method below; the begin method might be a trifle more clean for the purpose, though). The "default default" values for the encodings are registered here (see the CXUserDefaults class):

@implementation DialogueConversion
-void begin
{
    // just set the default values to the same ones as the TestConversion filter uses
    from=@","; to=@";";
    // register the default encoding values (see the CXString class)
    id enc=XNUM2OBJ([CXString defaultCStringEncoding]);
   [[[self currentFilter] filterDefaults] registerDefaults:[CXDictionary dictionaryWithObjectsAndKeys:
        enc,@"Input Encoding",
        enc,@"Output Encoding",
        nil]];
}
...

We have set as defaults the same find and replace values which were used by the previous filter: "," will be replaced by a ";". The input and ouptut streams both are by default presumed to be in the default C string encoding.

Now, the default values in the from and to variables were static strings; these we do not need to release. Though, in the details dialogue, the user might have edited them; if so, the contents of the variables got replaced by normal, retained strings. Those we should release when the conversion ends; thus we should implement the finish method with the following plain contents:

...
-void finish
{
    [from release];
    [to release];
}
...

The details method constructs and returns an array of the CXCDDI objects, representing the dialogue. The objects describe respectively:

...
-CXArray *details
{
    // get all the available encoding names; see again the CXString class
    CXArray *encNames=[CXString availableEncodingLocalizedNamesAndNumbers];
    // construct and return the details dialogue items
    CXArray *details=[CXArray arrayWithObjects:
        [CXCDDI string:@"Find string":&from],
        [CXCDDI string:@"Replace string":&to],
        nil];
   CXUserDefaults *df=[[self currentFilter] filterDefaults];
    // in case I am not reading from a file, I won't need to set the encoding
    if ([_input filename]!=nil)
        [details addObject:[CXCDDI list:@"Input encoding":encNames:@"Input Encoding",df]];
    //
in case I am not writing to a file, I won't need to set the encoding
    if ([_output filename]!=nil)
        [details addObject:[CXCDDI list:@"Output encoding":encNames:@"Output Encoding",df]];
    return details;
}
...

Note the array of the dialogue items is constructed truly dynamically: since the filter can read data not only from a file, but, say, from a clipboard, it is reasonable to check whether the input data is some file, and add the input encoding dialogue item only if so. Analogically for the output encoding.

Actually, these considerations are out of date. The current XConversion framework ensures automatic selection of the input and output encodings for all the relevant data types, without forcing the filter programmer to do so manually.

Now, so as to validate the from and to strings (it would be a nonsense to use an empty from one) the validateDetails method is implemented. The implementation is quite straighforward and primitive:

...
-CXString *validateDetails
{
    if ([from length]==0) return @"The find string can't be empty";
    return nil;
}
...

Finally, the conversion does not differ much from the one in the previous example; this time we only set the input and output encodings before we start:

...
-void doConvert
{
    CXCStreamData *inp=(CXCStreamData*)_input,*out=(CXCStreamData*)_output;
    CXString *line;

    // set the encodings (only in case a file is used)
   CXUserDefaults *df=[[self currentFilter] filterDefaults];
   if ([inp filename]!=nil) [inp setCurrentStringEncoding:
[df intForKey:@"Input Encoding"]];
    if ([out filename]!=nil) [out setCurrentStringEncoding:
[df intForKey:@"Output Encoding"]];

    [_progress start:[inp size]];
    [out writeFormat:@"// translated from <%@> by \"%@\"\n\n",[inp filename],[[self currentFilter] name]];
    while ((line=[inp readLine])!=nil) {
        CXAutoreleasePool *pool=[CXAutoreleasePool pool];

        [_progress step:[s length]];
        line=[[line componentsSeparatedByString:from] componentsJoinedByString:to];
        [out writeString:s];
        [out writeString:@"\n"];
        [pool release];
    }
}
@end

There is no need to show how the filter object would be made; the only difference from the previous example is the DialogueConversion class name instead of the TestConversion one.


An Epoc application filter

The last example shows a dictionary store filter, used for an Epoc application data file. For simplicity, we just open one Data application database, and create a new one, empty, but using the very same record structure. Besides, for the reader's benefit (and to show the XLog service), the filter would log the record structure out.

Since there are no property variables and no details dialogue, we will reimplement just the doConvert method:

@interface EpocDataConversion:CXCConversion
-void doConvert(CXCProgress *progress);
@end

The implementation is quite simple; the only slight complication is that the XSdk does not offer any object-based services for the application engines, thence the poor Epoc API must be used here. The auxiliary function typeName is quite plain, and just returns a name for an Epoc database field type:

@implementation EpocDataConversion
static CXString *typeName(int type)
{
    switch (type) {
       ...
    }
}
-void doConvert
{
    
CXCDictionaryStoreData *inp=(CXCDictionaryStoreData*)_input,*out=(CXCDictionaryStoreData*)_output;

    // the data layout; Epoc bloomin' API must be used here
    CDaModel *dbModelOld=CDaModel::NewL(*(CFileStore*)[inp store]);
    // the database layout description
    CDaUserDbDesc *desc=dbModelOld->UserDbDesc();
    if (desc) {
        TInt i,num=desc->ColumnCount();
        XLog(@"%d columns in the database \"%@\", table \"%S\":",num,[inp filename],&desc->TableName());
        for (i=1;i<=num;i++) {
            TDaUserCol cc=desc->Col(i);
            XLog(@"  \"%S\", type %@ (%d), len %d%s%s (%x)",
                cc.iName, typeName(cc.iType), cc.iType, cc.iMaxLength,
                (cc.iAttributes&1)?", NotNull":"",
                (cc.iAttributes&2)?", AutoIncrement":"",
                cc.iAttributes);
        }
    }
    // create a new model with the same layout
    CDaModel *dbModelNew=CDaModel::NewL();
    dbModelNew->CreateDatabaseL([out store],(CDaUserColSet*)&desc->ColSet());
    // we could copy the data now, but for simplicity we will skip that

    // store the new model into the output data
    RStoreWriteStream stream;
    [out writeEpocStreamFor:KUidDataHeadStream.iUid,(RDictionaryWriteStream&)stream];
    dbModelNew->ExternalizeL(stream);
    stream.CommitL();
    stream.Close();
    // thats all, we just should delete the allocated data
    delete dbModelOld;
    delete dbModelNew;
}
@end

Note that since the database application model uses the Epoc API, you must use the writeEpocStream method from the CXCDictionaryStoreData class, and commit / close the stream yourself. That would not be needed should you use the stream yourself, for then you would call the writeDataFor method instead, and let the returned data object be released (ie. commited and closed) automatically when you need them no more.

Note also that we have never addressed the proper setting of the store UIDs (in case it is a file store), or the application identifier stream: that all is done automatically by the XConversion framework.

Finally, the creation of the filter object will differ very slightly, for we have to prepare the data type appropriate to the Epoc Data application file:

id ordinal0neExport(void)
{
    CXCDataType *epocData=[CXCDataType dataTypeForEpocData];
    return [CXCFilter filterWith:
        epocData,epocData, // input and output data types
        EpocDataConversionClass, // the only conversion class
        @"Epoc Data Empty Filter", // the filter name...
        @"1.00", // ...version...
        @"OCSoftware", // ...creator...
        @"A Stupid Epoc Data Copy Filter, good for nothing"]; // ...and description
}


Copyright © 1999-2000 X.soft, all rights reserved