Click here to Skip to main content
11,629,393 members (70,216 online)
Click here to Skip to main content

Simple XML manipulation techniques within Android

, 11 Jul 2012 CPOL 26.7K 881 19
Rate this:
Please Sign up or sign in to vote.
In this article I will discuss how XML can be effectivelly processed inside an Android application

Introduction

In this article I am going to discuss the various posibilities of working with XML files for storing your own custom configurations. XML files are ideal for doing this sort of thing for a number of reasons...They absorb easily into Android Object Models (AOM) using the XML DOM or SAX parsers. As we'll soon see, in a future expansion of this article, XML configurations could also be generated and ingested into your objects via the SIMPLE method, which I have briefly touched on in my previous article.

.NET technique for converting large text files into XML structures intended for Android objects

XML does not even require a physical storage location on the Android device! This implies your configurations can be served on the fly through HTTP responses from a webservice or cloud service.

Just for the sake of an illustrative example, we can have one or more XML configuration files stored in the ASSETS folder initially and later modify these default configurations and have them moved to the device SD Card. This way, the end user will always have a default configuration to fall back on! I generally give end user the option to restore back to "the factory configuration" or more commonly refered to as "default config". What this simply translates to, is a one-step approach of deleting the SD Card config and the software will do all the smart checking for you to see if the user reverted back to default. Once the default config is modified, it gets copied back to the SD Card. The user always has a copy of the original footprint and that is never lost. It could actually come from a config server if so desired. I will discuss this attractive method in more detail later on. I also confess, this dual method is the way I generally prefer to work with XML config files rather than store configuration in alternate databases. Overall, I find XML to be my method of choice for storing meta-data, configuration specific data and as we saw in the previous article, I even use it in my e-reader application to absorb entire e-book sections into my baseline AOM. Please see my last article for further clarification on what this XML data store means. Please make sure to experiment with your own unique implementations as well, it will only help you master this technique...

Background

I will not take too much of your time discussing the exhaustive needs for implementing XML as a strategy for storing frequently changing information inside your app! I'm not going to advocate you use this technique over other tools in the Android arsenal, you can better make up your own mind according to your needs and application specifications. As I have mentioned before, I just seem to find XML favorable, because it is so easy to work with and extendable for a wide range of uses! Its structure can be quite intuitively simple and self descriptive, by its very nature if you just happen to build it that way...It can offer a good clue as to the final structure of your object model if that is not somehow already defined! Either way, one could easily traverse into the other! Sometimes, I like to think of my object model exactly in terms of the XML structure itself. I have provided a small example of the configuration which I have used in my e-book reader. This config contains all the Style Manager parameters used to model the fonts used throughout the body of the e-reader. This can obviously be evolved to contain a myriad of many other options the core application, but for now, lets just imagine it for exactly what I have designed it for and that is to format Font Styles inside the e-reader app. Lets take a closer look!

Using the code

<?xml version="1.0" encoding="utf-8"?>
<STYLEMANAGER>
<FONTSTYLES>
<DISPLAYTEXT>Book Title Font</DISPLAYTEXT>
<FONT>Celebration Text Fancy-Normal</FONT>
<FONTSIZE>37</FONTSIZE>
<FONTCOLOR>Ghost White</FONTCOLOR>
<FONTBOLD>False</FONTBOLD>
<FONTSHADOW></FONTSHADOW>
<DISPLAYTEXT>Book ID Font</DISPLAYTEXT>
<FONT>Caligrafia De Bula-Regio</FONT>
<FONTSIZE>28</FONTSIZE>
<FONTCOLOR>Black</FONTCOLOR>
<FONTBOLD>False</FONTBOLD>
<FONTSHADOW></FONTSHADOW>
<DISPLAYTEXT>Book Name Font</DISPLAYTEXT>
<FONT>Ballade Bold</FONT>
<FONTSIZE>27</FONTSIZE>
<FONTCOLOR>Red</FONTCOLOR>
<FONTBOLD>False</FONTBOLD>
<FONTSHADOW></FONTSHADOW>
<DISPLAYTEXT>Section Font</DISPLAYTEXT>
<FONT>Ballade Bold</FONT>
<FONTSIZE>15</FONTSIZE>
<FONTCOLOR>White</FONTCOLOR>
<FONTBOLD>False</FONTBOLD>
<FONTSHADOW></FONTSHADOW>
<DISPLAYTEXT>Verse Font</DISPLAYTEXT>
<FONT>Ballade Contour</FONT>
<FONTSIZE>25</FONTSIZE>
<FONTCOLOR>Aqua</FONTCOLOR>
<FONTBOLD>False</FONTBOLD>
<FONTSHADOW></FONTSHADOW>
</FONTSTYLES>
<SKINSTYLES>
<MAIN_BACKGROUND>Stationary Rust</MAIN_BACKGROUND>
<VERSES_BACKGROUND>Stationary Rust</VERSES_BACKGROUND>
<CLICKSELECTOR>Blue Selector with Bible</CLICKSELECTOR>
</SKINSTYLES>
</STYLEMANAGER>

The FontStyle Manager is broken down into two sections! From the XML structure we can determine that we have </FONTSTYLES> nodes and </SKINSTYLES> nodes. It is quite self-explanatory, that some sections only repeat themselves. In this configuration (5) five of such sections would consist of the various font style types. This is nice on many levels, because it makes the reader object generously simple by easily indexing through the presented information found in this particular config file. So how can this information be readily parsed and presented in object form? There are many ways to do this, but I will only present my own perspective on how this could be done. You might come up with a more creative option, to better suit your own needs!

// XMLParser.java

// Created by Mario Ghecea on 6/05/2012
// e-mail: solarasoft@gmail.com

// This is a simple XML DOM parsing utility
// which allows fetching, storing and loading of XML 
// configuration documents in a bi-directional methodology
// From Assets folder and saving and retrieving To and From SD Card.
// I offer this code copyright FREE for demonstrative and educational purposes!
// Feel free to modify and distribute as you wish! 
// If you decide to include its contents inside your 
// source, please leave a courtesy notice inside your code specifying that 
// I'm the original author! 

package com.solara.engineering.kjv.Parsers;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
 
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
 
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
 
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
 
public class XMLParser {
 
 // constructor
 public XMLParser() {
 
 }
 
 /**
  * Getting XML from URL making HTTP request
  * @param url string
  * */
 public String getXmlFromUrl(String url) {
  String xml = null;
 
  try {
   // defaultHttpClient
   DefaultHttpClient httpClient = new DefaultHttpClient();
   HttpPost httpPost = new HttpPost(url);
 
   HttpResponse httpResponse = httpClient.execute(httpPost);
   HttpEntity httpEntity = httpResponse.getEntity();
   xml = EntityUtils.toString(httpEntity);
 
  } catch (UnsupportedEncodingException e) {
   e.printStackTrace();
  } catch (ClientProtocolException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }
  // return XML
  return xml;
 }
 
 public String readXMLFromFile(Context activity, String xmlFile, boolean useConfigDir)
 {
  InputStream is = null;
  File file = null;
  File sdCard = null;
  Writer writer = new StringWriter();
  boolean exists = false;
 
  if (useConfigDir)
  {
   sdCard = Environment.getExternalStorageDirectory(); 
   file = new File (sdCard.getAbsolutePath() + "/kjv/config/" + xmlFile); 
   
   if (!(file == null)) 
   {
    if (exists = file.exists())
     try {
      is = new FileInputStream(file);
     } catch (FileNotFoundException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
   }
  }
  
  if (!exists)
  { 
    AssetManager assetManager = activity.getAssets(); 
    try {
    is = assetManager.open(xmlFile);
   } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } 
  }
   
  if (is != null) 
  {            
               
   char[] buffer = new char[1024];            
   try 
   {                
    Reader reader = new BufferedReader(                        
      new InputStreamReader(is, "UTF-8"));                
    int n;                
    while ((n = reader.read(buffer)) != -1) {                    
     writer.write(buffer, 0, n);                
     }
     is.close();
   }
   catch (IOException e) {
    Log.e("Error: ", e.getMessage());
    return null;
   }
  }
     
   
  return writer.toString();
  
 }
 
 public void writeXMLToFile(Context context, String xmlFile, String xmlData)
 {
   
         FileOutputStream fOut = null; 
 
         OutputStreamWriter osw = null;
         
 
         try
         {
          
          File sdCard = Environment.getExternalStorageDirectory(); 
          File dir = new File (sdCard.getAbsolutePath() + "/kjv/config");
          
          
          if (!dir.exists())
          {
           if (!(dir.mkdirs())) 
            Log.e("mkdirs", "Failed to create SDCARD mounted directory!!!");
          }
          
          
          fOut = new FileOutputStream(new File(dir, xmlFile)); 
          
           
  
          osw = new OutputStreamWriter(fOut); 
  
          osw.write(xmlData); 
  
          osw.flush(); 
  
          Toast.makeText(context, "Settings saved",Toast.LENGTH_SHORT).show();
 
          } 
 
   catch (Exception e) 
   {       
    e.printStackTrace(); 
    Toast.makeText(context, "Settings not saved",Toast.LENGTH_SHORT).show();
   } 
  
   finally 
   { 
     try
     { 
       osw.close(); 
       fOut.close(); 
     } 
     catch (IOException e) 
     { 
       e.printStackTrace(); 
     }
   }   
}
 
/**
  * Getting XML DOM element
  * @param XML string
* */
public Document getDomElement(String xml){
  Document doc = null;
  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  try {
 
   DocumentBuilder db = dbf.newDocumentBuilder();
 
   InputSource is = new InputSource();
          is.setCharacterStream(new StringReader(xml));
          doc = db.parse(is); 
 
   } catch (ParserConfigurationException e) {
    Log.e("Error: ", e.getMessage());
    return null;
   } catch (SAXException e) {
    Log.e("Error: ", e.getMessage());
             return null;
   } catch (IOException e) {
    Log.e("Error: ", e.getMessage());
    return null;
   }
 
   return doc;
}
 
/** Getting node value
   * @param elem element
   */
public final String getElementValue( Node elem ) {
      Node child;
      if( elem != null){
          if (elem.hasChildNodes()){
              for( child = elem.getFirstChild(); child != null; child = child.getNextSibling() ){
                  if( child.getNodeType() == Node.TEXT_NODE  ){
                      return child.getNodeValue();
                  }
              }
          }
      }
      return "";
}
  
/**
   * Getting node value
   * @param Element node
   * @param key string
* */
public String getValue(Element item, String str) {  
   NodeList n = item.getElementsByTagName(str);  
   return this.getElementValue(n.item(0));
}
  
public void setValue(Element elem, String str){
   Node child;
      if( elem != null){
          if (elem.hasChildNodes()){
              for( child = elem.getFirstChild(); child != null; child = child.getNextSibling() ){
                  if( child.getNodeType() == Node.TEXT_NODE  ){
                      child.setNodeValue(str);
                  }
              }
          }
      }
}
  
public String GetElementAttribute(Element item, String attribName){
   return item.getAttribute(attribName);   
}
}

The code I have listed above is a helper class called XMLParser.java which is used to load various XML configs through the two methods I have specified above! One method, getXmlFromUrl() will fetch it from an web server as an HTTP request for a file resource! The other method, readXMLFromFile() will fetch the XML config from a file stored in the Assets folder. The AssetManager will open and return this file as an InputStream object. This is further subdivided into independent fixed size chunks inside a Reader buffer and written out in much the same way. In the end you get your concatenated XML config that was just read from the original file, in a useful string format. The boolean useConfigDir, when set to True, specifies that you want to use the SD Card directory instead. Method writeXMLToFile() will conversely, allow you to save these various XML configs to SD Card and does all the required sanity checking for you! Feel free to expand and modify these functions to your heart's content! They are not perfect by any means and are only used as a template mechanism for loading and saving XML configs. More efficient and perhaps faster means of doing this may be available...Keep me informed if you come up with alternatives that work better and faster! This was my quick fix and it worked relatively well for my application! As final closing statements on the helper code, I should mention that the last few functions are only there for convenience and are responsible for either getting the XML Dom object hierarchy consisting of all the element nodes or otherwise getting and setting values inside the element nodes. That's all there is to it! In the next section we will discuss a simple implementation of this helper class!

// KjvFontStyle.java

// Created by Mario Ghecea on 6/05/2012
// e-mail: <a href="mailto:solarasoft@gmail.com">solarasoft@gmail.com</a>

// This is an implementation of the XML DOM parsing utility!
// It offers bi-directional config file access methodologies
// SD Card access and has been extended to offer an XML Serializer

// I offer this code copyright FREE for demonstrative and educational purposes!
// Feel free to modify and distribute as you wish! 
// If you decide to include its contents inside your 
// source, please leave a courtesy notice inside your code specifying that 
// I'm the original author! 

 
package com.solara.engineering.kjv.StyleManager;
 
import java.io.StringWriter;
 
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
 
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
 
import com.solara.engineering.kjv.Parsers.XMLParser;
 

import android.content.Context;
 
public class KjvFontStyle
{
  static final int maxPos = 4;
  private int position = 0;
  private String displayText;
  private String font;
  private String fontSize;
  private String fontColor;
  private String fontBold;
  private String mainBackground;
  private String versesBackground;
  private String clickSelector;
  
  static final String TAG_FONTSTYLES = "FONTSTYLES";
  static final String TAG_SKINSTYLES = "SKINSTYLES";
  static final String TAG_DISPLAYTEXT = "DISPLAYTEXT";
  static final String TAG_FONT = "FONT";
  static final String TAG_FONTSIZE = "FONTSIZE";
  static final String TAG_FONTCOLOR = "FONTCOLOR";
  static final String TAG_FONTBOLD = "FONTBOLD";
  static final String TAG_MAIN_BACKGROUND = "MAIN_BACKGROUND";
  static final String TAG_VERSES_BACKGROUND = "VERSES_BACKGROUND";
  static final String TAG_CLICKSELECTOR = "CLICKSELECTOR";
     
  XMLParser parser;
  Document doc;
  NodeList nl;
  Element e;
  Context context;
  String xml;
  
  // constructor
  public void LoadFontStyle(Context activity)
  {
   context = activity;
   
   parser = new XMLParser();
   xml = parser.readXMLFromFile(activity,"stylemanager.xml", true);
 
   doc = parser.getDomElement(xml); // getting DOM element  
  
  
   nl = doc.getElementsByTagName(TAG_DISPLAYTEXT);
   Element e = (Element) nl.item(position);
   displayText = parser.getValue(e, TAG_DISPLAYTEXT);
   nl = doc.getElementsByTagName(TAG_FONT);
   e = (Element) nl.item(position);
   font = parser.getValue(e, TAG_FONT);
   nl = doc.getElementsByTagName(TAG_FONTSIZE);
   e = (Element) nl.item(position);
   fontSize = parser.getValue(e, TAG_FONTSIZE);
   nl = doc.getElementsByTagName(TAG_FONTCOLOR);
   e = (Element) nl.item(position);
   fontColor = parser.getValue(e, TAG_FONTCOLOR);
   nl = doc.getElementsByTagName(TAG_FONTBOLD);
   e = (Element) nl.item(position);
   fontBold = parser.getValue(e, TAG_FONTBOLD);
   nl = doc.getElementsByTagName(TAG_MAIN_BACKGROUND);
   e = (Element) nl.item(0);
   mainBackground = parser.getValue(e, TAG_MAIN_BACKGROUND);
   nl = doc.getElementsByTagName(TAG_VERSES_BACKGROUND);
   e = (Element) nl.item(0);
   versesBackground = parser.getValue(e, TAG_VERSES_BACKGROUND);
   nl = doc.getElementsByTagName(TAG_CLICKSELECTOR);
   e = (Element) nl.item(0);
   clickSelector = parser.getValue(e, TAG_CLICKSELECTOR);
    
   //if (position <= maxPos)
   //position ++;
  
  }
  
  public void SetPosition(int pos){
   if (pos >= 0 && pos <= maxPos)
    position = pos;
  }
  
  // Property getters
  
  public int GetMaxPosition()
  {
   return maxPos;
  }
  
  public int GetPosition()
  {
  return position; 
  }
  
  public void ResetPosition()
  {
   position = 0;
  }
  
  public String GetDisplayText()
  {
  return displayText; 
  }
  
  public String GetFont()
  {
   return font;
  }
  
  public String GetFontSize()
  {
   return fontSize;
  }
  
  public String GetFontColor()
  {
   return fontColor;
  }
  
  public String GetFontBold()
  {
   return fontBold;
  }      
  
  // Property Setters
  
  public String GetMainBackground()
  {
   return mainBackground;
  }
  
  public String GetVersesBackground()
  {
   return versesBackground;
  }
  
  public String GetClickSelector()
  {
   return clickSelector;
  }
  
  
  public void SetFont(String fontValue)
  {    
   nl = doc.getElementsByTagName(TAG_FONT);
   e = (Element) nl.item(position);
   parser.setValue(e, fontValue); 
   font = parser.getValue(e, TAG_FONT);
  }
  
  public void SetFontSize(String fontSizeValue)
  {    
   nl = doc.getElementsByTagName(TAG_FONTSIZE);
   e = (Element) nl.item(position);
   parser.setValue(e, fontSizeValue);
   fontSize = parser.getValue(e, TAG_FONTSIZE);
 }
  
  public void SetFontColor(String fontColorValue)
  {
    
    nl = doc.getElementsByTagName(TAG_FONTCOLOR);
    e = (Element) nl.item(position);
    parser.setValue(e, fontColorValue); 
    fontColor = parser.getValue(e, TAG_FONTCOLOR);
  }
  
  public void SetFontBold(String fontBoldValue)
  {
   nl = doc.getElementsByTagName(TAG_FONTBOLD);
   e = (Element) nl.item(position);
   parser.setValue(e, fontBoldValue);
   fontBold = parser.getValue(e, TAG_FONTBOLD);
  } 
  
  public void SetMainBackground(String mainBackgroundImage)
  {
   nl = doc.getElementsByTagName(TAG_MAIN_BACKGROUND);
   e = (Element) nl.item(0);
   parser.setValue(e, mainBackgroundImage);
   mainBackground = parser.getValue(e, TAG_MAIN_BACKGROUND);
  }
  
  public void SetVersesBackground(String versesBackgroundImage)
  {
   nl = doc.getElementsByTagName(TAG_VERSES_BACKGROUND);
   e = (Element) nl.item(0);
   parser.setValue(e, versesBackgroundImage);
   versesBackground = parser.getValue(e, TAG_VERSES_BACKGROUND);
  }
  
  public void SetClickSelector(String clickSelectorImage)
  {
   nl = doc.getElementsByTagName(TAG_CLICKSELECTOR);
   e = (Element) nl.item(0);
   parser.setValue(e, clickSelectorImage);
   clickSelector = parser.getValue(e, TAG_CLICKSELECTOR);
  }
  
  public void SaveXMLSettings()
  {
   String serializedXML = this.SerializeXML(doc);
   parser.writeXMLToFile(context, "stylemanager.xml", serializedXML );
  }
  
  public String SerializeXML (Document doc)
  {
 
  // create Transformer object

  Transformer transformer = null;
  try {
   transformer = TransformerFactory.newInstance().newTransformer();
  } catch (TransformerConfigurationException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (TransformerFactoryConfigurationError e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  StringWriter writer = new StringWriter();
  StreamResult result = new StreamResult(writer);
  try {
   transformer.transform(new DOMSource(doc), result);
  } catch (TransformerException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  // return XML string
  return writer.toString();
  }
}

In this section of code, I proceed to demonstrate the implementation of the XML Helper class by encapsulating its use in a KjvFontStyle object. I have also decided to include as a free gift, an XML Serializer since DOM did not posess one internally! This becomes quite useful when modifying many of the internal node values and attributes inside the original XML configuration. Without the bonus serializer, you would otherwise be burdened with having to devise a clever way to stringiffy this information back into the ubiquitous String format! In a nutshell, this is what all those Transformer object manipulations inside the Serializer are for! Well, I think I might have exhausted the topic of this article by now, and I hope that you have found these code listings useful! They could hopefully provide you with some insight into the nature of the beast! The neat thing is that once you have done the main body of work using this parsing framework by adding your own unique improvements, of course, it could be easily scaled to access an unlimited number of XML configurations of your own choosing! One last topic I wanted to touch on briefly, as I promissed at the beginning of this article, is the beneficial use of the SIMPLE framework! But since we ran out of space in this article, I will refrain from cluttering this section with anymore lengthy snippets of code from my somewhat extensive e-reader framework! I will reveal more tips on these Android XML ingestion techniques in the next forth coming series quite soon! Please check back often and if you find any of these articles useful, drop me a line and don't forget to give me some form of a rating nevertheless... See you all again soon here on CP!

History

First revision 7/11/2012.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mario Ghecea
Software Developer (Senior) Solara Software
United States United States
I am a senior software developer native to San Diego, CA. with about 25+ years of coding experience on various platforms including Windows and Linux, but who has spent the last part of his career involved in firmware development in R&D labs throughout the country. I particularly enjoy coding in C++, C# and lately JAVA on the Android platform. I like to wear many hats and have gotten down at very low levels such as writing VHDL logic for FPGAs and even some driver writing on Windows, Linux and TI DSPs. I have worked with many microcontrollers just because I love that low-level bit banging code! When I'm not coding or playing with Android tablets, phones and firmware kits you will most likely find me riding my Suzuki Hayabusa crotch-rocket out in the hills. I also enjoy composing music, singing or just relaxing and hanging out at small coffee shops around town!

You may also be interested in...

Comments and Discussions

 
QuestionThis logic I could use to create layouts coming from a xml? Pin
Member 1002484120-May-13 2:23
memberMember 1002484120-May-13 2:23 
AnswerRe: This logic I could use to create layouts coming from a xml? Pin
Mario Ghecea17-Jan-14 10:44
memberMario Ghecea17-Jan-14 10:44 
GeneralMy vote of 4 Pin
Jason Leatherface16-Jul-12 11:47
memberJason Leatherface16-Jul-12 11:47 
All Caps! Ugh! Good otherwise.
GeneralRe: My vote of 4 Pin
Mario Ghecea17-Jan-14 10:44
memberMario Ghecea17-Jan-14 10:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150723.1 | Last Updated 11 Jul 2012
Article Copyright 2012 by Mario Ghecea
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid