Powerful filtering on collections in Flex
People search for things all the time. It is in our genes and we use tools to achieve our goals. On the internet a lot of us use Google to find what we need. So most of us are familiar with Google search techniques. Now some of these search techniques are put into a very powerful filter function for Flex.
The features supported
- Synonyms
Synonyms can be supplied to the filter. A synonym is an alternative word for a word used in your query. In the demo application “four” is added as synonym for “4″. So if you type four the album 4 by Johan is found.
- Search the exact word (using “”)
If you do not want to include a synonym in your search you can use double quotes around the word for an exact match. If you type “four” the album 4 by Johan is not found in the demo application.
- Phrase search (using “”)
This feature allows you to search for the exact words in the exact same order as you have put them between the double quotes. When you type “of the” the Otis Redding album and the Pink Floyd album are the results in the demo application.
- Exclude terms (using -)
A minus sign before a word will make the filter select the data not containing the word. When you type -pink In the demo application, the result will contain all the albums except for the one by Pink Floyd.
- The OR operator (using OR or |)
By default all the terms of a search have to be in the data. Technically the filter is using the AND operator in the search. If you want to the filter to return the results from either of the terms, you can use the OR operator (OR or |). For example otis | lenny will result in the albums by Otis Redding and Lenny Kravitz in the demo application.
- Grouping terms (using ())
Brackets can be used to group terms the way you want it to search.
- Case sensitive filtering
Google searches everything case insensitive. So a feature to enable or disable this might be unexpected. I use this quite often. So i could not resist putting it in, but you can leave it out of course. If the option is off, searching for the will also find the album by The Cure in the demo application. If the option is on the album by The Cure will not be found.
This filter has an ignore list too. If you add words to it, you can make it even more Google search like. For example Google ignores words like “a” or “the” to help their users in their quest for information.
Of course you do not have to use features like the ignore list, the synonyms and the case sensitive option. If you do not use these features you still have a very powerful filter that’s easy to implement. A nice feature to extend this filter with in your application would be to allow the user to select the columns to search in. This column selection is not in the demo application, but my guess is you are perfectly capable to do it yourself.
The source
The only code shown is the code of the demo application, but you can download the full source. The other code is not put on the page, because it are too many files to put on a webpage. Do not let this scare you, because it is not that much or that complicated.
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
creationComplete="creationCompleted(event)"
width="400" height="265">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.events.FlexEvent;
import nl.vanhulzenonline.collections.filter.Evaluator;
import nl.vanhulzenonline.demo.Album;
import spark.components.gridClasses.GridColumn;
private var _collection:ArrayCollection;
private var _evaluator:Evaluator = new Evaluator();
private function creationCompleted(event:FlexEvent):void
{
_collection = Album.collection;
_collection.filterFunction = filterCollection;
_evaluator.synonyms["four"] = new ArrayCollection(["4"]);
grid.dataProvider = _collection;
}
private function filterChanged(event:Event):void
{
update();
}
private function update():void
{
_evaluator.prepare(filter.text);
_collection.refresh();
formula.text = (_evaluator.tree) ? _evaluator.tree.toString() : "";
}
private function filterCollection(data:Object):Boolean
{
var labels:ArrayCollection = new ArrayCollection();
for (var i:int; i < grid.columns.length; i++)
{
labels.addItem(
(grid.columns.getItemAt(i) as GridColumn).itemToLabel(data));
}
return _evaluator.evaluate(labels);
}
private function caseSensitiveFilterChanged(event:FlexEvent):void
{
_evaluator.caseSensitive = caseSensitiveFilter.selected;
update();
}
]]>
</fx:Script>
<s:VGroup left="5" right="5" bottom="5" top="5" >
<s:TextInput id="filter" width="100%" change="filterChanged(event)" />
<s:CheckBox id="caseSensitiveFilter" label="filter case sensitive"
valueCommit="caseSensitiveFilterChanged(event)" />
<s:DataGrid id="grid" width="100%" height="100%" >
</s:DataGrid>
<s:Label id="formula" />
</s:VGroup>
</s:Application>
Fast searching in a ByteArray
For some reason there is no method to search for a specific text in a ByteArray. At least i could not find one. I tried a very straightforward search by simply scanning from every position. Although it works, it is very slow with bigger ByteArrays.
A quick look on the internet resulted in an algorithm that suited my needs: the Boyer-Moore-Horspool algorithm. Unfortunately there was no ActionScript alternative available for it yet. So i decided to port it myself and with great result, because it greatly improved the performance of the searches for my use case.
package nl.vanhulzenonline.utils
{
import flash.utils.ByteArray;
public class ByteArrayUtils
{
public static function getIndexOf(text:String, data:ByteArray, start:int = 0, end:int = -1):int
{
var pattern:ByteArray = new ByteArray();
pattern.writeUTFBytes(text);
pattern.position = 0;
if (end == -1)
end = data.length - 1;
var i:int;
var badCharSkip:Array = new Array();
// initialize the table to default value
// when a character is encountered that does not occur
// in the pattern, we can safely skip ahead for the whole
// length of the pattern.
for (i = 0; i <= 255; i++)
badCharSkip[i] = pattern.length;
// then populate it with the analysis of the pattern
var endOfPattern:int = pattern.length - 1;
for (i = 0; i < endOfPattern; i = i + 1)
badCharSkip[pattern.readUnsignedByte()] = endOfPattern - i;
// do the matching
// search the data, while the pattern can still be within it.
var dataPart:int;
var endOfData:int = end;
var dataPosition:int = start;
while (endOfData >= endOfPattern)
{
// scan from the end of the pattern
i = endOfPattern;
while(true)
{
data.position = dataPosition + i;
pattern.position = i;
if (data.readUnsignedByte() == pattern.readUnsignedByte())
{
// if the first byte matches, we've found it.
if (i == 0)
return dataPosition;
i--;
}
else
break;
}
// otherwise, we need to skip some bytes and start again.
// note that here we are getting the skip value based on
// the last byte of pattern, no matter where we didn't
// match. so if pattern is: "abcd" then we are skipping
// based on 'd' and that value will be 4, and for "abcdd"
// we again skip on 'd' but the value will be only 1.
// the alternative of pretending that the mismatched
// character was the last character is slower in the normal
// case (eg. finding "abcd" in "...azcd..." gives 4 by
// using 'd' but only 4-2==2 using 'z'.
data.position = dataPosition + endOfPattern;
dataPart = data.readUnsignedByte();
endOfData -= badCharSkip[dataPart];
dataPosition += badCharSkip[dataPart];
}
return -1;
}
}
}
XML configuration of Log4j for the ServletHelper
The original ServletHelper was introduced in Separate a J2EE application from data/configuration. In short it is a generic way to make a J2EE application configurable. Just use the ServletHelper class to have easy log and application configuration and have the application data outside your deployed J2EE application. So redeploying your .war file won’t destroy your data or configuration.
The previous ServletHelper only supported a Log4j .properties file. This new version of the ServletHelper supports XML configuration of Log4j too. If the data/configuration directory contains a file name [servletname]-log4j.xml, it is used to configure Log4j for the J2EE application using the new version of the ServletHelper.
Check the original article for more information on how to use the ServletHelper.
package nl.vanhulzenonline.servlethelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletContext;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;
public class ServletHelper
{
public static Logger logger = null;
public static Properties properties = null;
private static ServletContext _context = null;
private static String getServletName()
{
return _context.getContextPath().substring(1);
}
private static String getServletHomePathVariableName()
{
return getServletName().toUpperCase() + "_HOME";
}
private static String getServletHomePath()
{
String path = System.getenv(getServletHomePathVariableName());
return (path == null) ? "" : path;
}
private static String getPropertiesFilePrefix()
{
return getServletHomePath() + File.separatorChar + getServletName().toLowerCase();
}
private static String getLog4jPropertiesFileName()
{
return getPropertiesFilePrefix() + "-log4j.properties";
}
private static String getLog4jXMLFileName()
{
return getPropertiesFilePrefix() + "-log4j.xml";
}
private static String getServletPropertiesFileName()
{
return getPropertiesFilePrefix() + ".properties";
}
private static Properties loadProperties(File file)
{
try
{
Properties properties = new Properties();
FileInputStream stream = new FileInputStream(file);
properties.load(stream);
stream.close();
return properties;
}
catch (Exception e)
{
logger.error("Failure reading " + getServletPropertiesFileName() + ". Unable to execute " + getServletName() + ".");
return null;
}
}
private static void initializeLog4j()
{
logger = Logger.getLogger(getServletName());
// we have to initialize log4j first
File file = new File(getLog4jXMLFileName());
if (file.exists())
{
DOMConfigurator.configure(getLog4jXMLFileName()); // configure log4j
logger.info("Using " + getLog4jXMLFileName() + " for log4j configuration.");
}
else
{
file = new File(getLog4jPropertiesFileName());
if (file.exists())
{
PropertyConfigurator.configure(getLog4jPropertiesFileName()); // configure log4j
logger.info("Using " + getLog4jPropertiesFileName() + " for log4j configuration.");
}
else
{
BasicConfigurator.configure(); // configure log4j
logger.info("Using default log4j configuration.");
}
}
}
private static Boolean initializeServlet()
{
// checking the home path
String homePath = getServletHomePath();
if (homePath == "")
{
logger.error(getServletHomePathVariableName() + " is not set. Unable to execute " + getServletName() + ".");
return false;
}
File homeDir = new File(homePath);
if (!homeDir.exists())
{
logger.error(getServletHomePath() + " does not exist. Unable to execute " + getServletName() + ".");
return false;
}
if(!homeDir.isDirectory())
{
logger.error(getServletHomePath() + " is not a directory. Unable to execute " + getServletName() + ".");
return false;
}
// checking the properties file of the webapp and load the properties
File propertiesFile = new File(getServletPropertiesFileName());
if (propertiesFile.exists())
{
properties = loadProperties(propertiesFile);
if (properties == null)
return false;
}
else
{
logger.error(getServletPropertiesFileName() + " not found. Unable to execute " + getServletName() + ".");
return false;
}
logger.info(getServletName() + " initialized.");
return true;
}
public static Boolean initialize(ServletContext context)
{
if (_context != null)
return false;
_context = context;
initializeLog4j();
return initializeServlet();
}
}

