Working with Adobe AIR one often needs to read text files from the user's file system. The available methods of the FileStream object do not provide a convenient way to achieve the functionality of reading the file line bye line. Thus, I came up with an extended implementation of the FileStream class that supports reading line by line, aptly called, FileStreamWithLineReader.

To read a line from a text file,
var file:File = new File("fileToBeRead.txt");
var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
stream.open(file, FileMode.READ);

while(stream.bytesAvailable) {
    var line:String = stream.readLine();
    trace(line);
}

The class does not have big detrimental effect on the performance of read operations, but still I would recommend using it only for files when one needs to read the entire file line by line.

Hope this helps.
~ Keep Walking.


/**
 *
 * as3extensions - ActionScript Utility Classes
 * Copyright (C) 2010-2011, myJerry Developers
 * http://www.myjerry.org/as3extensions
 *
 * The file is licensed under the the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.myjerry.as3extensions.io
{
 import flash.filesystem.FileStream;
 import flash.geom.PerspectiveProjection;
 import flash.utils.ByteArray;
 
 /**
  * An extension of the flash.filesystem.FileStream object that supports
  * reading lines from the inherent file stream. The objects uses an internal buffer so
  * as not to hit the performance. The default buffer size is 512 bytes.
  * 
  * The object supports only standard methods of the underlying FileStream
  * object.
  * 
  * Usage of the class is only recommended when one tends to read the data line by line
  * from a file stream. If the readUTFLine() or readMultiByteLine()
  * methods are not intended to be called, it will be preferable to use the FileStream
  * object, for performance reasons only.
  * 
  * @author Sandeep Gupta
  * @version 1.0
  * @since 17 Dec 2010
  */
 public class FileStreamWithLineReader extends FileStream {
  
  /**
   * The internal buffer we maintain to be used when working with the
   * readLine() method.
   */
  private var buffer:ByteArray = new ByteArray();
  
  /**
   * The default buffer size.
   */
  private var bufferSize:uint = 512;
  
  /**
   * Constructor
   */
  public function FileStreamWithLineReader() {
   super();
  }
  
  /**
   * Set the buffer size to the given value. A value of ZERO will reset the buffer size
   * to a default value of 512 bytes.
   */
  public function setBufferSize(value:uint):void {
   if(value == 0) {
    // the trace line here is intentional to let developer's know
    // of the default value in debug mode.
    trace('FileStreamWithLineReader: Buffer set to default size of 512 bytes.');
    this.bufferSize = 512;
    return;
   }
   
   this.bufferSize = value;
  } 
  
  /**
   * Utility method to see if check if we have data in our internal buffer.
   */
  protected function get hasBuffer():Boolean {
   if(buffer.length == 0) {
    return false;
   }
   
   return true;
  }
  
  /**
   * Method that returns the number of bytes that can be read in the next read
   * over the actual file stream. The value is the lesser amongst the buffer size
   * and the actual number of bytes available in the file stream.
   */
  protected function get bytesToRead():uint {
   return Math.min(this.bufferSize, super.bytesAvailable);
  }
  
  /**
   * Method to refill the buffer again with data from the file stream. The buffer is
   * either filled with the number of bytes as returned by the bytesToRead()
   * method.
   */
  protected function refillBuffer():void {
   if(!this.hasBuffer) {
    super.readBytes(buffer, 0, this.bytesToRead);
   }
  }
  
  /**
   * Method to clean up the current buffer and move back in the original stream by the
   * extra bytes that were buffered in.
   */
  protected function undoBuffer():void {
   if(this.hasBuffer) {
    super.position = (super.position - this.buffer.length);
    this.buffer = new ByteArray();
   }
  }
  
  /**
   * Reads a UTF-8 encoded line from the inherent file stream. The presence of
   * a line termination character '\n' indicates the end-of-line.
   */
  public function readUTFLine():String {
   return readMultiByteLine("utf-8");
  }
  
  /**
   * Reads a line in the specified encoding from the inherent file stream. The presence of
   * a line termination character '\n' indicates the end-of-line.
   */
  public function readMultiByteLine(charSet:String):String {
   var toReturn:String = readLine(charSet);
   
   // the following check is a fix when on windows the buffer reads between the values of
   // 13 and 10, which are used to indicate the end of line
   if(toReturn != null && toReturn.charCodeAt(toReturn.length - 1) == 13) {
    return toReturn.substr(0, toReturn.length - 1);
   }
    
   return toReturn;
  }
  
  /**
   * Method that actually attempts to read a line from the inherent file stream using internal buffers
   * in the given encoding. If no line termination character is found, the entire stream from the current
   * position to the end-of-file is returned back.
   */
  protected function readLine(charSet:String):String {
   var joinedString:String = '';
   var toReturn:String = '';
   var extraReadString:String = ''; 
   do {
    refillBuffer();

    var currentReadString:String = this.buffer.readMultiByte(this.buffer.length, charSet);
    
    // try and see if current buffer has enough data to return a line
    var index:int = currentReadString.indexOf('\n');
    if(index != -1) {
     if(index == 0) {
      // indicates that the first character of the current read string is a new line
      // return joined string
      toReturn = joinedString;
     } else {
      toReturn = joinedString + currentReadString.substr(0, index - 1);
     }

     extraReadString = currentReadString.substr(index + 1); 
     this.buffer = new ByteArray();
     this.buffer.writeMultiByte(extraReadString, charSet);
     this.buffer.position = 0;
     
     return toReturn;
    }
    
    // check if we have more data to read from the file
    // if not, we can return from this point
    if(this.bytesToRead == 0) {
     this.buffer = new ByteArray();
     return joinedString + currentReadString;
    } 
    
    // no the current buffer does not has enough data
    // move the current read string to joined string
    joinedString += currentReadString;
    this.buffer = new ByteArray();
   } while(true);
   
   return joinedString;
  }
  
  /**
   * Returns the number of bytes of data available for reading in the input buffer.
   */
  override public function get bytesAvailable():uint {
   if(this.hasBuffer) {
    return buffer.length + super.bytesAvailable;
   }
   
   return super.bytesAvailable;
  }
  
  /**
   * The current position in the buffer/file where the next read will happen.
   */
  override public function get position():Number {
   if(this.hasBuffer) {
    return this.position - buffer.length;
   }
   
   return super.position;
  }
  
  /**
   * Resets the next reading position in the inherent stream and adjusts the internal buffers
   * accordingly.
   */
  override public function set position(value:Number):void {
   if(this.hasBuffer) {
    if(value > this.buffer.length) {
     // empty the buffer
     // and skip in original stream
     buffer = new ByteArray();
     value = value - buffer.length;
     super.position = value;
     return;
    } 
    
    if(value == buffer.length) {
     // empty the buffer
     // no need to skip
     buffer = new ByteArray();
     return;
    }
    
    // we just need to skip inside the buffer
    // get how many positions do we need to skip
    value = this.buffer.length - value;
    var tempBuffer:ByteArray = new ByteArray();
    this.buffer.readBytes(tempBuffer, value);
    this.buffer = tempBuffer;
    return;
   }
   
   super.position = value;
  }
  
  /**
   * Overriden parent class functions to support buffering
   */
  
  
  override public function readBoolean():Boolean {
   refillBuffer();
   
   return this.buffer.readBoolean();
  }
  
  override public function readByte():int {
   refillBuffer();
   
   return this.buffer.readByte();
  }
  
  override public function readBytes(bytes:ByteArray, offset:uint=0, length:uint=0):void {
   undoBuffer();
   
   super.readBytes(bytes, offset, length);
  }
  
  override public function readDouble():Number {
   refillBuffer();
   
   return this.buffer.readDouble();
  }
  
  override public function readFloat():Number {
   refillBuffer();
   
   return this.buffer.readFloat();
  }
  
  override public function readInt():int {
   refillBuffer();
   
   return this.buffer.readInt();
  }
  
  override public function readObject():* {
   undoBuffer();
   
   return super.readObject();
  }
  
  override public function readShort():int {
   refillBuffer();
   
   return this.buffer.readShort();
  }
  
  override public function readUnsignedByte():uint {
   refillBuffer();
   
   return this.buffer.readUnsignedByte();
  }
  
  override public function readUnsignedInt():uint {
   refillBuffer();
   
   return this.buffer.readUnsignedInt();
  }
  
  override public function readUnsignedShort():uint {
   refillBuffer();
   
   return this.buffer.readUnsignedShort();
  }
  
  override public function readUTF():String {
   undoBuffer();
   
   return super.readUTF();
  }
  
  override public function readUTFBytes(length:uint):String {
   undoBuffer();
   
   return super.readUTFBytes(length);
  }
 }
}

written by Sandeep Gupta

Monday, December 20, 2010 at 2:12 PM

Comments

8 responses to ' ActionScript: Read text file line by line, introducing readLine() '

  • This is a fine piece of code! I do have a question though. I made an app that users have to be able to define their own buttons. I figured I would have a buttons.txt with a different label on each line. I want to create a loop to read a line, create a button and assign that line as the label. I think your code is suited for this but am having trouble visualizing how to do it. Any hints?

  • Hi Casey, it should be easy. Try something like below inside a visual container,

    var file:File = new File("fileToBeRead.txt");
    var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
    stream.open(file, FileMode.READ);

    while(stream.bytesAvailable) {
        var line:String = stream.readLine();

        var button:Button = new Button();
        button.label = line;
        button.width = 200;
        button.height = 50;

        this.addElement(button);
    }


    Hope this helps!

  • Thanks sandy! That's very easy. What if every second line I have a string that I want the the button to insert into a textarea?

    For example I have a text file structured like this:

    buttons.txt
    button1_label
    "Hello"
    button2_label
    "Goodbye"
    button3_label
    "Come again"

    So every second line contains the string for the button's listener.

    Here's my code so far:

    public function setupBtns(e:Event):void{
    var file:File = new File("fileToBeRead.txt");
    var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
    stream.open(file, FileMode.READ);

    while(stream.bytesAvailable) {
    var line:String = stream.readLine();

    var tempBtn:Button = new Button();
    tempBtn.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void{

    });
    tempBtn.label = line;
    tempBtn.width = 200;
    tempBtn.height = 50;
    btnArray.push(tempBtn);
    }
    }

  • Casey, you may ignore every alternate line when reading form line before you assign that as the button label.

    public function setupBtns(e:Event):void{
        var file:File = new File("fileToBeRead.txt");
        var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
        stream.open(file, FileMode.READ);

        while(stream.bytesAvailable) {
            // this line contains the headers like button1_label etc.
            var line:String = stream.readLine();

            if(stream.bytesAvailable) {
                // this line contains the actual label like "Hello";
                line = stream.readLine();
                
                // strip off the first and last character as they are double quotes
                line = line.subString(1, line.length - 1);
                
                var tempBtn:Button = new Button();
                tempBtn.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void{
                    // do something
                });
                
                tempBtn.label = line;
                tempBtn.width = 200;
                tempBtn.height = 50;
                btnArray.push(tempBtn);
            }
        }
    }


    Hope this helps.

  • Thanks Sandy! I almost got it working but one small problem. All 3 buttons have the same output, the last String that was assigned. "Come again".

    public function setupBtns(e:Event):void{
    var file:File = File.documentsDirectory.resolvePath("buttons.txt");
    var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
    stream.open(file, FileMode.READ);

    while(stream.bytesAvailable) {
    // this line contains the headers like button1_label etc.
    var label:String = stream.readUTFLine();
    var line:String;

    if(stream.bytesAvailable) {
    // this line contains the actual label like "Hello";
    line = stream.readUTFLine();

    // strip off the first and last character as they are double quotes
    line = line.substring(1, line.length-1);

    var tempBtn:Button = new Button();
    tempBtn.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void{
    trace(line);
    });

    tempBtn.label = label;
    btnArray.push(tempBtn);
    for(var i:Number = 0;i<btnArray.length;i++){
    group.addElement(btnArray[i]);
    }
    }
    }
    }

  • Very close Sandy :) The only problem remaining is that each button has the same output "Come again". The buttons all show up perfectly, and are labeled correctly.

    public function setupBtns(e:Event):void{
    var file:File = File.documentsDirectory.resolvePath("buttons.txt");
    var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
    stream.open(file, FileMode.READ);

    while(stream.bytesAvailable) {
    // this line contains the headers like button1_label etc.
    var label:String;
    var line:String;

    if(stream.bytesAvailable) {
    // this line contains the actual label like "Hello";
    label = stream.readUTFLine();
    line = stream.readUTFLine();

    // strip off the first and last character as they are double quotes
    line = line.substring(1, line.length-1);

    var tempBtn:Button = new Button();
    tempBtn.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void{
    trace(line);
    });

    tempBtn.label = label;
    btnArray.push(tempBtn);
    for(var i:Number = 0;i<btnArray.length;i++){
    group.addElement(btnArray[i]);
    }
    }
    }
    }

  • Oops sorry for the double post

  • Finally go it Sandy and thought I'd share for anyone else trying to do the same.


    public function setupBtns(e:Event):void{
    var file:File = File.documentsDirectory.resolvePath("buttons.txt");
    var stream:FileStreamWithLineReader = new FileStreamWithLineReader();
    stream.open(file, FileMode.READ);

    while(stream.bytesAvailable) {
    // this line contains the headers like button1_label etc.
    var label:String;
    // this line contains the string

    if(stream.bytesAvailable) {
    // this line contains the actual label like "Hello";
    label = stream.readUTFLine();
    line = stream.readUTFLine();

    // strip off the first and last character as they are double quotes
    line = line.substring(1, line.length-1);

    var tempBtn:Button = new Button();
    tempBtn.addEventListener(MouseEvent.CLICK, btnListener);

    tempBtn.label = label;
    tempBtn.name = line;
    btnArray.push(tempBtn);
    for(var i:Number = 0;i<btnArray.length;i++){
    group.addElement(btnArray[i]);
    }
    }
    }
    }

    protected function btnListener(e:MouseEvent):void{
    mainTextField.insertText(e.target.name);
    trace(e.target.name);
    }

    Thanks for all the help and this great piece of code!

Post Comments