Using properties file

  โดย  ประดับเก่ง

 
  เกริ่นนำ
ในการเขียน Java Application (Standalone) หรือ Servlet (Server side) ที่ต้องมีการ initialize ค่าตัวแปรต่าง ๆ ที่เราไม่สามารถใส่ค่าที่แน่นอนได้ในช่วง compile time เช่น ชื่อของไดร์เวอร์ที่โปรแกรมใช้สำหรับติดต่อกับเดต้าเบส, ค่าของ host ที่โปรแกรมเราจะติดต่อ หรือแม้กระทั่งค่าของ port ที่โปรแกรมจะใช้ สิ่งหนึ่งที่จะขาดไม่ได้คือ properties file. 
Properties file คือ ไฟล์ที่ใช้เก็บชื่อ*และค่า (key-value pairs) ของตัวแปรต่าง ๆ ที่เราอาจต้องทำการแก้ไขในภายหลัง ซึ่งมักจะเกิดขึ้นในช่วงหลังจากที่เราได้ทำการคอมไพล์ไฟล์ java ให้กลายเป็น .class แล้ว.  properties file จะช่วยให้เราสามารถเปลี่ยนค่าตัวแปรต่าง ๆ ก่อนที่จะทำการรันโปรแกรม ซึ่งค่าของตัวแปรเหล่านี้จะถูกโหลดเข้าไปในโปรแกรมในช่วง runtime แทนที่จะต้อง hardcode ลงไปในโปรแกรม และต้องทำการ recompile ใหม่ทุกครั้งที่มีการแก้ไขค่าตัวแปรเหล่านี้
* ชื่อ ในที่นี้ก็คือ กุญแจ (key) ใน Hashtable เพราะชื่อและค่าของตัวแปรต่าง ๆ ที่เก็บอยู่ใน properties file จะถูกตีความและเก็บไว้ใน java.util.Properties ซึ่งเป็น subclass ของ java.util.Hashtable

การเขียน Properties file 
จริง ๆ แล้ว properties file ก็คือ text ไฟล์ธรรมดา ๆ แต่นักพัฒนาส่วนมากมักนิยมใช้นามสกุลที่เป็น xxx.properties เพื่อความชัดเจนในการจำแนกไฟล์นี้ออกจากไฟล์อื่น ๆ (ในปัจจุบัน properties file ได้เริ่มมีการเปลี่ยนลักษณะการเก็บ โดยใช้ XML format แทน ซึ่งจะกล่าวถึงในบทความต่อ ๆ ไป)
โดยทั่วไป หนึ่งบรรทัดใน properties file จะใช้เก็บชื่อและค่า (key-value pairs) ของหนึ่งตัวแปร  ซึ่งแต่ละบรรทัดจะจบด้วย line terminator (\n, \r หรือ \r\n) โดยจะถูกใส่ลงไปอัตโนมัติโดย text editor เมื่อเรากด enter เพื่อขึ้นบรรทัดใหม่.  บรรทัดไหนที่ว่าง หรือขึ้นต้นด้วย # หรือ ! จะถูกข้ามไปโดยอัตโนมัติ ซึ่งปกติเราจะใช้สำหรับการเขียน comment 
properties file จะถูกตีความโดยคลาส java.util.Properties สิ่งที่ใช้เป็นตัวบอกคลาสดังกล่าว เพื่อแยกแยะชื่อและค่าของตัวแปร ซึ่งถูกเก็บอยู่ในแต่ละบรรทัด ออกจากกันก็คือ =, :, หรือ whitespace(" ") ยกตัวอย่างเช่น

# properties file for jarticles website 
serverName=www.jarticles.com 
port=7001
protocol=https

หรือ
# properties file for jarticles website
serverName:www.jarticles.com 
port:7001
protocol:https

หรือ 
# properties file for jarticles website
serverName www.jarticles.com 
port 7001
protocol https

หรือแม้กระทั่ง
# properties file for jarticles website
serverName=www.jarticles.com 
port:7001
protocol https

Note: คลาส java.util.Properties จะแยกชื่อและค่าออกจากกันโดยใช้=, :, หรือ whitespace(" ") ที่เจอตัวแรกเท่านั้น ถ้าปรากฎว่ามีตัวพวกนี้ปรากฎขึ้นมาอีกหลังจากนั้น ตัวที่เหลือพวกนี้จะกลายเป็นส่วนหนึ่งของค่า (value) ของตัวแปรไป ยกตัวอย่างเช่น

# properties file with more than 1 "="
myhost=http://127.0.0.1=localhost

จะได้ (key, value) = ("myhost", "http://127.0.0.1=localhost")
ซึ่งในกรณีนี้ เราสามารถนำมาใช้ประโยชน์ในการเก็บตัวแปรที่มีค่ามากกว่าหนึ่งค่าได้ โดยการแยก http://127.0.0.1=localhost ออกจากกันอีกทีหนึ่ง โดย search ไปที่ตัว = แล้วแยกสองค่านี้ออกจากกัน

ในกรณีที่ชื่อและค่าของตัวแปรไม่สามารถเขียนอยู่ในบรรทัดเดียวได้ หรือถ้าต้องการเขยิบลงไปอีกบรรทัดหนึ่ง เราก็สามารถใช้ \ ใส่ลงไปที่ท้ายสุดของบรรทัดแรก เพื่อเป็นตัวบอกว่าบรรทัดต่อไปยังคงเป็นส่วนหนึ่งของบรรทัดปัจจุบัน (โดยช่องว่างหลัง \ และก่อนตัวอักษรตัวแรกของบรรทัดต่อไป จะถูกตัดออกไปทั้งหมด) ยกตัวอย่างเช่น

# properties file for jarticles website
serverName=www.jarticles.com
port=\
      7001
protocol=https

จะได้ 
(key, value) = ("serverName", "www.jarticles.com")
(key, value) = ("port", "7001")
(key, value) = ("protocol", "https");

ในกรณีที่มีช่องว่างที่อยู่ข้างหน้าหรือข้างหลัง ชื่อ (key) และ ค่า (value) ของตัวแปร ช่องว่างเหล่านี้จะถูกตัดออกไปโดยอัตโนมัติ ยกตัวอย่างเช่น

# properties file for jarticles website 
serverName=www.jarticles.com 
   port = 7001
protocol=https

จะได้ 
(key, value) = ("serverName", "www.jarticles.com")
(key, value) = ("port", "7001")
(key, value) = ("protocol", "https")
* ตรงบรรทัด port=7001 จะสังเกตเห็นว่ามีช่องว่างอยู่ข้างหน้าและหลังคำว่า port และ 7001 แต่จะถูกตัดออกไป 

อีกสองตัวอย่าง ซึ่งนำมาจาก JDK API

Truth = Beauty
    Truth:Beauty
Truth                         Beauty
3 อันข้างบนจะได้ผลเหมือนกันคือ (key, value) = ("Truth","Beauty") หรือ

fruits                           apple, banana, pear, \
                                 cantaloupe, watermelon, \
                                 kiwi, mango

จะได้ (key, value) = ("fruit", "apple, banana, pear, cantaloupe, watermelon, kiwi, mango")

Advance properties file
ถ้าใครเคยดู properties file ของ apache webserver หรือ jserv servlet engine จะเห็นอะไรคล้าย ๆ อย่างนี้

support .Z .z .tgz .gz .zip
servlet.dbdemo.initArgs=\
               username=paul,\
               password=Youtellme1,\
               owner=www.jarticles.com

หลังจากที่ java.util.Properties ทำการตีความแล้ว จะได้ผลดังนี้
(key, value) = ("support", ".Z .z .tgz .gz .zip")
(key, value) = ("servlet.dbdemo.initArgs", "username=paul,password=Youtellme1,owner=www.jarticles.com")

เราจะเห็นว่า ใน value จะประกอบด้วยค่าหลาย ๆ ค่า ซึ่ง format จะขึ้นอยู่กับนักพัฒนาที่ออกแบบระบบนั้น ๆ  ในการดึงค่าทั้งหมดออกมา เราจะต้องทำการตีความ value ที่ได้เอง ซึ่งจะทำได้โดยใช้ java.util.StringTokenizer (ตัวอย่างการเขียนจะอยู่ในส่วนของ programming ข้างล่าง)

How to program
ในการเขียนโปรแกรมเพื่อโหลดชื่อและค่าของตัวแปรต่าง ๆ ที่อยู่ใน propeties file เข้ามาในโปรแกรม มีขั้นตอนหลัก ๆ อยู่ 2 ขั้นดังนี้ คือ
1) โหลด properties file เข้ามาอยู่ในรูปของ InputStream
2)  ใช้ java.util.Properties.load(InputStream input) อ่าน InputStream ที่ได้เข้ามาที่คลาส Properties เพื่อทำการตีความ

สมมุติว่าเรามีไฟล์ชื่อ server.properties ซึ่งเก็บค่าตัวแปร 4 ตัว ดังต่อไปนี้
# our first properties file 
ServerType=standalone
Timeout=300
KeepAlive=On
MaxKeepAliveRequests=100

โปรแกรมที่ง่ายที่สุด  สำหรับโหลดไฟล์ดังกล่าว ก็จะเป็นอย่างโปรแกรมข้างล่าง (PropertiesEx1.java)

import java.io.*;
import java.util.Properties;

public class PropertiesEx1 {

 public static void main(String args[]) {
  Properties props = null;

  try {
   FileInputStream input = new FileInputStream("D:\\Mystuff\\server.properties");
   props = new Properties();
   props.load(input);
  } catch (Exception e) {
    e.printStackTrace(); 
  }

  System.out.println("ServerType=" + props.getProperty("ServerType"));
  System.out.println("Timeout=" + props.getProperty("Timeout"));
  System.out.println("KeepAlive=" + props.getProperty("KeepAlive"));
  System.out.println("MaxKeepAliveRequests=" + props.getProperty("MaxKeepAliveRequests"));
 }

*ส่วนที่เป็น HighLight เป็นส่วนที่แสดง 2 ขั้นตอนหลัก อย่างที่กล่าวมาข้างต้น
ก่อนอื่น เราต้องการโหลดไฟล์ server.properties ซึ่งอยู่ที่ D:\Mystuff\server.properties ดังนั้นเราจึงใช้ FileInputStream ซึ่งเป็น subclass ของ InputStream ในการอ่านไฟล์  จะสังเกตเห็นว่า String ที่เป็น input  argument สำหรับ FileInputStream จะเป็น  "D:\\Mystuff\\server.properties" แทนที่จะเป็น "D:\Mystuff\server.properties" เหตุผลคือ ตัว  \(backslash) เป็นตัวที่ต้องมีการ escape  sequence ในจาว่า ดังนั้นเราต้องใช้ \\  แทน 
จากนั้นเราทำการสร้าง object ของ java.util.Properties แล้วทำการโหลด server.properties ผ่านฟังก์ชั่น Properties.load(InputStream inStream) โดยเมื่อไฟล์ของเราผ่านการตีความโดยคลาส Properties แล้ว เราสามารถอ่านค่าต่าง ๆ  ที่อยู่ในไฟล์ server.properties โดยผ่านฟังก์ชั่น Properties.getProperty(String key)

ข้อควรจำคือ ในการอ่านค่าต่าง ๆ โดยใช้ฟังก์ชั่น getProperty(String key) เราจะต้องใช้ key ที่เหมือนกับชื่อของตัวแปรที่อยู่ใน properties file ของเรา (case sensitive) เพราะ java.util.Properties เป็น subclass ของ Hashtable ยกตัวอย่างเช่น

System.out.println("ServerType=" + props.getProperty("ServerType"));
จะได้ ServerType=standalone

แต่ System.out.println("ServerType=" + props.getProperty("serverType"));
จะได้ ServerType=null

ถ้าในกรณีที่ server.properties อยู่ package เดียวกับคลาสที่ใช้ในการอ่านตัวมัน  เช่น D:\Mystuff\PropertiesEx1.class และ D:\Mystuff\server.properties เราก็จะไม่ต้อง hardcode ที่อยู่ของไฟล์ server.properties โดยให้ FileInputStream โหลดที่ 

FileInputStream input = new FileInputStream("server.properties");

ซึ่ง default คือ ให้โหลดที่ current directory ที่ PropertiesEx1.class อยู่แทน

java.util.StringTokenizer
บางทีในตัวแปรหนึ่ง  เราอาจต้องการเก็บค่ามากกว่าหนึ่งค่า เช่นตัวแปร support ที่อยู่ในไฟล์ support.properties

# support.properties file
support .Z .z .tgz .gz .zip

จะเห็นว่า ตัวแปร support จะมีค่า 5 ค่า คือ .Z, .z, .tgz, .gz และ .zip
ขั้นตอนการเขียนโปรแกรมให้อ่าน support.properties สามารถทำได้ดังนี้คือ
1) อ่านไฟล์ server.properties เข้ามาที่ FileInputStream เหมือนปกติ
2)  ให้คลาส Properties ทำการโหลด และตีความ
3) หลังจากขั้นตอนที่สอง จะได้ (key, value) ออกมาเป็น ("support", ".Z .z .tgz .gz .zip") ซึ่งทั้ง 5 ค่าของ support ยังติดกันอยู่โดยมี " " เป็นตัวขั้น ดังนั้นให้ใช้ java.util.StringTokenizer แยกค่าดังกล่าวออกจากกัน โดยเซ็ต delimiter = " " ดังโปรแกรมข้างล่าง (PropertiesEx2.java)

import java.io.*;
import java.util.Properties;
import java.util.StringTokenizer;

public class PropertiesEx2 {

 public static void main(String args[]) {
  Properties props = null;

  try {
   FileInputStream input = new FileInputStream("support.properties");
   props = new Properties();
   props.load(input);
  } catch (Exception e) {
    e.printStackTrace(); 
  }

  String supportStr = props.getProperty("support");
  StringTokenizer st = new StringTokenizer(supportStr, " ");
  while (st.hasMoreTokens()) {
   System.err.println("File format supported=" + st.nextToken());
    } 
  }

StringTokenizer จะทำการแยกสตริง supportStr ออกเป็นส่วน ๆ โดยใช้ " " เป็นตัวแบ่ง ซึ่งในกรณีของเรา จะได้สตริง 5 ตัว แต่ละตัวที่ได้ก็คือ ค่าทั้ง 5 ของ support 
ในการแยกสตริงออกจากกันโดยใช้ StringTokenizer เราสามารถดูว่ายังมีสตริงเหลือจากการแยกอยู่หรือเปล่าโดยใช้ฟังก์ชั่น StringTokenizer.hasMoreTokens() ซึ่งถ้า return ค่าที่เป็น true ออกมา เราก็สามารถอ่านค่าของสตริงตัวที่เหลือนั้นได้ โดยใช้ฟังก์ชั่น StringTokenizer.nextToken() ซึ่งค่าที่ออกมาจะอยู่ในรูปของ String

ตัวอย่างอีกอันของ advance properties file ที่เคยกล่าวมาข้างต้น
servlet.dbdemo.initArgs=\
               username=paul,\
               password=Youtellme1,\
               owner=www.jarticles.com

จะเห็นว่า servlet.dbdemo.initArgs ไม่ได้เป็นแค่ key ที่เก็บค่าเพียง 1 ค่า แต่ตัวมันเองกลับเป็นตัวที่เก็บชื่อและค่าอีก 3 คู่เอาไว้ ซึ่งก็คือ 
servlet.dbdemo.initArgs => username=paul
                        => password=Youtellme1
                        => owner=www.jarticles.com

ขั้นตอนการเขียนโปรแกรม เพื่อทำการตีความ อาจเป็นดังนี้คือ
1) แยก (key, value) โดยใช้ Properties.load(InputStream inStream) ออกมาเป็น ("servlet.dbdemo.initArgs","username=paul,password=Youtellme1,owner=www.jarticles.com") ก่อน 
2) หลังจากนั้น  ให้ใช้ StringTokenizer แยกชื่อและค่าออกมา โดยใช้ตัว "," เป็นตัวแยก ก็จะได้
string1 = "username=paul"
string2 = "password=Youtellme1"
string3 = "owner=www.jarticles.com"
3)  ทำการแยก key, value ของแต่ละ string ออกจากกันโดย StringTokenizer โดยใช้ตัว "=" เป็นตัวแยกอีกที ซึ่งจะได้
(key, value) = ("usernameusername", "paul")
(key, value) = ("password, "Youtellme1")
(key, value) = ("owner", "www.jarticles.com")

ตัวอย่างของโปรแกรมที่ใช้โหลดไฟล์ servletInfo.properties จะเป็นอย่างข้างล่างนี้ (PropertiesEx3.java)
import java.io.*;
import java.util.*;

public class PropertiesEx3 {

 public static void main(String args[]) {
  Properties props = null;
  Vector servletList = new Vector();

  try {
   FileInputStream input = new FileInputStream("servletInfo.properties");
   props = new Properties();
   props.load(input);
  } catch (Exception e) {
    e.printStackTrace(); 
  }

  for (Enumeration e = props.keys() ; e.hasMoreElements() ;) {
   // key = "servlet.dbdemo.initArgs"
   // value = "username=paul,password=Youtellme1,owner=www.jarticles.com"
   String key = (String) e.nextElement(); 
   String value = (String) props.get(key);

   ServletInfo servletInfo = new ServletInfo(key);
   StringTokenizer st = new StringTokenizer(value, ",");
   String nvPair = null;
   while ( st.hasMoreTokens() ) {
    // nvPair => username=paul or password=Youtellme1 or owner=www.jarticles.com
    nvPair = st.nextToken(); 
    StringTokenizer st1 = new StringTokenizer(nvPair, "="); 
    String tmpName = null;
    String tmpValue = null;
    int i=0;

    while ( st1.hasMoreTokens() ) {
     if (i == 0) {
      tmpName = st1.nextToken(); 
      i++;
     } else {
      tmpValue = st1.nextToken(); 
     } 
    }
    servletInfo.addProperty(tmpName, tmpValue);
   }
   servletList.add(servletInfo);
  } 

  int vSize = servletList.size();
  if (vSize > 0) {
   ServletInfo servletInfoTemp = null;
   for (int i=0; i<vSize; i++) {
    servletInfoTemp = (ServletInfo) servletList.elementAt(i); 
    servletInfoTemp.listProperties();
   } 
  }
 }

class ServletInfo {
 String servletName = null;
 Hashtable props = null;

 public ServletInfo(String name) {
  servletName = name; 
  props = new Hashtable();
 } 

 public void addProperty(String name, String value) {
  props.put(name, value); 
 }

 public void listProperties() {
  if ( props.isEmpty() ) {
   System.out.println("ServletInfo: " + servletName + " has no property"); 
  } else {
   String key = null;
   System.out.println("ServletInfo: " + servletName + " has propertie(s)...");
   for (Enumeration e = props.keys() ; e.hasMoreElements() ;) {
    key = (String) e.nextElement();
       System.out.println("(key, value) = (\"" + key + "\",\"" + props.get(key) + "\")");
      } 
  }
 }
}


Copyright (C) 2000 www.jarticles.com.