com.npower.wurfl.Wurfl.java Source code

Java tutorial

Introduction

Here is the source code for com.npower.wurfl.Wurfl.java

Source

/**
  * $Header: /home/master/nWave-DM-Common/src/com/npower/wurfl/Wurfl.java,v 1.3 2007/11/14 06:18:55 zhao Exp $
  * $Revision: 1.3 $
  * $Date: 2007/11/14 06:18:55 $
  *
  * ===============================================================================================
  * License, Version 1.1
  *
  * Copyright (c) 1994-2007 NPower Network Software Ltd.  All rights reserved.
  *
  * This SOURCE CODE FILE, which has been provided by NPower as part
  * of a NPower product for use ONLY by licensed users of the product,
  * includes CONFIDENTIAL and PROPRIETARY information of NPower.
  *
  * USE OF THIS SOFTWARE IS GOVERNED BY THE TERMS AND CONDITIONS
  * OF THE LICENSE STATEMENT AND LIMITED WARRANTY FURNISHED WITH
  * THE PRODUCT.
  *
  * IN PARTICULAR, YOU WILL INDEMNIFY AND HOLD NPower, ITS RELATED
  * COMPANIES AND ITS SUPPLIERS, HARMLESS FROM AND AGAINST ANY CLAIMS
  * OR LIABILITIES ARISING OUT OF THE USE, REPRODUCTION, OR DISTRIBUTION
  * OF YOUR PROGRAMS, INCLUDING ANY CLAIMS OR LIABILITIES ARISING OUT OF
  * OR RESULTING FROM THE USE, MODIFICATION, OR DISTRIBUTION OF PROGRAMS
  * OR FILES CREATED FROM, BASED ON, AND/OR DERIVED FROM THIS SOURCE
  * CODE FILE.
  * ===============================================================================================
  */

package com.npower.wurfl;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeMap;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Comment;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.Node;
import nu.xom.ParsingException;
import nu.xom.Serializer;
import nu.xom.ValidityException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Luca Passani (passani@eunet.no)
 * 
 * Wurfl class initializes by parsing the WURFL and capturing a bunch of info
 * about capabilities, devices and UA strings into convenience HashMaps,
 * Arraylists and so on. THIS ASSUMES WURFL is CORRECT. Uncorrect Wurfl will
 * produce unpredictable results. Use utility CheckWURFL.java to validate your
 * WURFL file
 * 
 * @version $Revision: 1.3 $ $Date: 2007/11/14 06:18:55 $
 */
public class Wurfl {

    private static Log log = LogFactory.getLog(Wurfl.class);

    // root parsed WURFL
    Document wurflDocument = null;

    // Hastables and lists used internally by the WURFL
    private Elements devices = null; // array

    // of all
    // devices

    int numberOfDevices = 0;

    private HashSet<String> deviceIdSet = null; // for

    // fast
    // Device
    // ID
    // lookup

    private HashMap<String, Element> deviceElementsList = null; // for

    // fast
    // access
    // to
    // device
    // elements

    private TreeMap<String, Element> actualDeviceElementsList = null; // for

    // fast
    // access
    // to
    // device
    // elements

    private HashSet<String> wurfl_actual_device_roots; // Actual

    // Device
    // Roots

    private HashMap<String, String> listOfUAWithDeviceID = null; // associate

    // UA
    // string
    // with
    // device
    // ID

    private HashMap<String, ArrayList<String>> listOfGroups = null; // HashMap

    // of
    // Arraylist
    // (capabilities
    // grouped
    // by
    // 'group')

    private ArrayList<String> listOfCapabilities = new ArrayList<String>(350);

    private HashSet<String> setOfCapabilityNames = null;

    int numberOfCapabilities = 0;

    private HashMap<String, String> genericCapabilityNameValues = new HashMap<String, String>(350);

    // patch file
    // private boolean patchfile_present = false;

    // private boolean patchfile_found = false;

    // private Element patch_root = null;

    /**
     * Initializes the WURFL fields.
     * 
     * @param wurflStream
     *            InputStream containing WURFL
     * @param patchStream
     *            InputStream containing the patch file (can be null)
     * 
     */
    private void init(InputStream wurflStream, InputStream patchStream) {

        try {
            Builder parser = new Builder();
            wurflDocument = parser.build(wurflStream);

            if (patchStream != null) {
                boolean patch_successful = true;
                Document patchedWurflDocument = (Document) wurflDocument.copy();
                try {
                    // Builder patch_parser = new Builder();
                    Document patchDocument = parser.build(patchStream);
                    Element patch_root = patchDocument.getRootElement();
                    Element wurfl_root = patchedWurflDocument.getRootElement();

                    String localname = patch_root.getLocalName();
                    if (!localname.equals("wurfl_patch")) {
                        throw new WurflException("patch file must have <wurfl_patch> as root tag");
                    }

                    // if there is a patch file, we parse the WURFL normally
                    // then we modify the object model.

                    ArrayList<String> wurfl_listOfCapabilities = new ArrayList<String>(250);
                    HashMap<String, String> wurfl_genericCapabilityNameValues = new HashMap<String, String>(250);
                    // HashSet wurfl_setOfCapabilityNames = null;
                    HashSet<String> wurfl_actual_device_roots = new HashSet<String>();

                    int wurfl_numberOfCapabilities = 0;
                    Element devices_elem = wurfl_root.getFirstChildElement("devices");
                    Elements wurfl_devices = devices_elem.getChildElements("device");
                    int wurfl_numberOfDevices = wurfl_devices.size();
                    // initialize arrayLists and HashMaps
                    HashSet<String> wurfl_deviceIdSet = new HashSet<String>(wurfl_numberOfDevices);
                    HashMap<String, Element> wurfl_deviceElementsList = new HashMap<String, Element>(6000, 0.75f);
                    HashMap<String, String> wurfl_listOfUAWithDeviceID = new HashMap<String, String>(6000, 0.75f);

                    for (int j = 0; j < wurfl_numberOfDevices; j++) {
                        String _devID = wurfl_devices.get(j).getAttributeValue("id");
                        // String _devFallBack =
                        // wurfl_devices.get(j).getAttributeValue("fall_back");
                        String _devUA = wurfl_devices.get(j).getAttributeValue("user_agent");
                        wurfl_deviceElementsList.put(_devID, wurfl_devices.get(j));
                        wurfl_deviceIdSet.add(_devID);
                        if ("true".equals(wurfl_devices.get(j).getAttributeValue("actual_device_root")))
                            wurfl_actual_device_roots.add(_devID);
                        wurfl_listOfUAWithDeviceID.put(_devUA, _devID);
                    }

                    // find capabilities (no checks because of basic assumption that WURFL
                    // is correct)
                    Element wurfl_genericElement = (Element) wurfl_deviceElementsList.get("generic");
                    Elements wurfl_groups = wurfl_genericElement.getChildElements("group");

                    // extra list to keep capabilities grouped by group
                    HashMap<String, ArrayList<String>> wurfl_listOfGroups = new HashMap<String, ArrayList<String>>(
                            wurfl_groups.size(), 1);

                    for (int i = 0; i < wurfl_groups.size(); i++) {
                        Elements wurfl_capaList = wurfl_groups.get(i).getChildElements("capability");
                        // group by group function
                        ArrayList<String> tmp_capalist = new ArrayList<String>(wurfl_capaList.size());

                        wurfl_numberOfCapabilities += wurfl_capaList.size();
                        for (int j = 0; j < wurfl_capaList.size(); j++) {
                            Element capa = wurfl_capaList.get(j);
                            wurfl_listOfCapabilities.add(capa.getAttributeValue("name"));
                            tmp_capalist.add(capa.getAttributeValue("name"));
                            wurfl_genericCapabilityNameValues.put(capa.getAttributeValue("name"),
                                    capa.getAttributeValue("value"));
                        }
                        wurfl_listOfGroups.put(wurfl_groups.get(i).getAttributeValue("id"), tmp_capalist);
                    }

                    // optimization to speed things up later: look-up in O(1) NOT O(n)
                    // wurfl_setOfCapabilityNames = new HashSet(listOfCapabilities);

                    /*
                     * Start analyzing the patch file The object model of wurfl-patch.xml
                     * is traversed to enrich the existing object model in wurfl.xml We
                     * need to do a bunch of checks here because the patch is not
                     * validated as the WURFL
                     */
                    Element devices_tag_patch = patch_root.getFirstChildElement("devices");
                    if (devices_tag_patch == null) {
                        String msg = "Illegal syntax patch file: <devices> tag is missing!";
                        patch_parse_error(msg);
                    }
                    Elements patch_devices = devices_tag_patch.getChildElements("device");
                    // loop through all devices in patch file
                    for (int j = 0; j < patch_devices.size(); j++) {
                        // log.debug("--------------------------------------");
                        Element current_patch_device = patch_devices.get(j);
                        String devID = current_patch_device.getAttributeValue("id");
                        // log.debug("analyzing device "+devID);
                        Element current_wurfl_device = (Element) wurfl_deviceElementsList.get(devID);

                        String fallback = current_patch_device.getAttributeValue("fall_back");
                        // soma basic checks on the consistency of each device
                        if (fallback == null || fallback.equals("")) {
                            String msg = "device " + devID + " in patch file does not have"
                                    + " a valid fallback value";
                            patch_parse_error(msg);
                        }
                        String ua = current_patch_device.getAttributeValue("user_agent");
                        if (ua == null || (ua.equals("") && !devID.equals("generic"))) {
                            String msg = "device " + devID + " in patch file does not have"
                                    + " a valid user_agent string";
                            patch_parse_error(msg);
                        }

                        // existing device or NEW Device?
                        if (wurfl_deviceIdSet.contains(devID)) {
                            log.debug("existing device.");
                            // modify existing device
                            if (!devID.equals("generic")) {
                                // first a bunch of basic checks
                                // Disallow modification of UA (but not fall_back)
                                String local_fallback = current_wurfl_device.getAttributeValue("fall_back");
                                if (!fallback.equals(local_fallback)) {
                                    Attribute fb = new Attribute("fall_back", local_fallback);
                                    current_wurfl_device.addAttribute(fb);
                                }
                                String local_ua = current_wurfl_device.getAttributeValue("user_agent");
                                if (!ua.equals(local_ua)) {
                                    String msg = "device " + devID + ". Sorry. Patch file devices are not allowed"
                                            + " to override user-agent. If you need to do that, please define a new device"
                                            + " in the patch file.";
                                    patch_parse_error(msg);
                                }

                                if ("true".equals(current_patch_device.getAttributeValue("actual_device_root"))) {
                                    wurfl_actual_device_roots.add(devID);
                                }

                            }

                            // need an array to test if group exists from before or not
                            Elements wurfl_element_groups = current_wurfl_device.getChildElements("group");
                            ArrayList<String> wurfl_element_groups_set = new ArrayList<String>(
                                    wurfl_element_groups.size());
                            for (int k = 0; k < wurfl_element_groups.size(); k++) {
                                wurfl_element_groups_set.add(wurfl_element_groups.get(k).getAttributeValue("id"));
                            }

                            // loop through all the groups
                            log.debug("Need to merge groups ");
                            Elements local_groups = current_patch_device.getChildElements("group");
                            for (int k = 0; k < local_groups.size(); k++) {
                                Element local_group = local_groups.get(k);
                                String local_group_id = local_group.getAttributeValue("id");
                                log.debug("group " + local_group_id);
                                if (local_group_id == null || local_group_id.equals("")) {
                                    String msg = "patch file, device " + devID + ": group without ID";
                                    patch_parse_error(msg);
                                }
                                if (!wurfl_element_groups_set.contains(local_group_id)) {
                                    log.debug("group " + local_group_id + " is not in the " + devID + " device.");
                                    // new group, attach as is
                                    Node tmp_group = local_group.copy();
                                    current_wurfl_device.appendChild(tmp_group);
                                } else {
                                    // existing group. Merge capabilities
                                    log.debug("group " + local_group_id + " IS in the " + devID + " device.");
                                    log.debug("We need to merge...");
                                    Element wurfl_group = null;
                                    for (int h = 0; h < wurfl_element_groups.size(); h++) {
                                        log.debug("Comparing " + wurfl_element_groups.get(h).getAttributeValue("id")
                                                + " and " + local_group.getAttributeValue("id"));

                                        if (wurfl_element_groups.get(h).getAttributeValue("id")
                                                .equals(local_group_id)) {
                                            wurfl_group = wurfl_element_groups.get(h);
                                            log.debug(
                                                    "Found" + wurfl_element_groups.get(h).getAttributeValue("id"));
                                        }
                                    }
                                    log.debug("Merging " + wurfl_group.getAttributeValue("id") + " and "
                                            + local_group.getAttributeValue("id"));
                                    merge_group_capabilities(wurfl_group, local_group);
                                }
                            } // end for loop

                        } else {
                            log.debug("New device...just copy");
                            Element dev_copy = (Element) current_patch_device.copy();
                            devices_elem.appendChild(dev_copy);
                        }

                    }

                } catch (Exception e) {
                    log.error(e.getMessage());
                    patch_successful = false;
                }
                if (patch_successful) {
                    log.info("Patching OK. Applied");
                    // try {toPrettyXML(patchedWurflDocument,System.out);} catch
                    // (Exception e) {}
                    wurflDocument = patchedWurflDocument;
                } else {
                    log.info("Patching failed; reverting to origional wurfl");
                }
            } // end of 'if (patchstream != null)'

            Element devices_elem = wurflDocument.getRootElement().getFirstChildElement("devices");

            // At this point, patchfile or not, we have a complete object model
            // of the wurfl.xml file. Let's build the ArrayLst and HashMaps for fast
            // look-up
            // by the API

            // Now it's time to re-visit the modified object model
            devices = devices_elem.getChildElements("device");
            numberOfDevices = devices.size();
            // initialize arrayLists and HashMaps
            deviceIdSet = new HashSet<String>(numberOfDevices);
            deviceElementsList = new HashMap<String, Element>(6000, 0.75f);
            listOfUAWithDeviceID = new HashMap<String, String>(6000, 0.75f);
            wurfl_actual_device_roots = new HashSet<String>(numberOfDevices);
            for (int j = 0; j < numberOfDevices; j++) {
                String _devID = devices.get(j).getAttributeValue("id");
                // String _devFallBack = devices.get(j).getAttributeValue("fall_back");
                String _devUA = devices.get(j).getAttributeValue("user_agent");
                deviceElementsList.put(_devID, devices.get(j));
                deviceIdSet.add(_devID);
                listOfUAWithDeviceID.put(_devUA, _devID);
                if ("true".equals(devices.get(j).getAttributeValue("actual_device_root")))
                    wurfl_actual_device_roots.add(_devID);
            }

            // find capabilities (no checks because of basic assumption that WURFL is
            // correct)
            Element genericElement = (Element) deviceElementsList.get("generic");

            Elements groups = genericElement.getChildElements("group");

            // extra list to keep capabilities grouped by group
            listOfGroups = new HashMap<String, ArrayList<String>>(groups.size(), 1);
            listOfCapabilities = new ArrayList<String>(350);
            genericCapabilityNameValues = new HashMap<String, String>(350);
            for (int i = 0; i < groups.size(); i++) {
                Elements capaList = groups.get(i).getChildElements("capability");
                // group by group function
                ArrayList<String> tmp_capalist = new ArrayList<String>(capaList.size());

                numberOfCapabilities += capaList.size();
                for (int j = 0; j < capaList.size(); j++) {
                    Element capa = capaList.get(j);
                    listOfCapabilities.add(capa.getAttributeValue("name"));
                    tmp_capalist.add(capa.getAttributeValue("name"));
                    genericCapabilityNameValues.put(capa.getAttributeValue("name"),
                            capa.getAttributeValue("value"));
                }
                listOfGroups.put(groups.get(i).getAttributeValue("id"), tmp_capalist);
            }

            // Tidy up our Lists.
            listOfCapabilities.trimToSize();

            // optimization to speed things up later: look-up in O(1) NOT O(n)
            setOfCapabilityNames = new HashSet<String>(listOfCapabilities);
            log.info("WURFL has been initialized");

        } catch (ValidityException ex) {
            System.err.println("WURFL is not valid");
            ex.printStackTrace();
            throw new WurflException("WURFL is not valid");
        } catch (ParsingException ex) {
            System.err.println("cannot parse the wurfl");
            ex.printStackTrace();
            throw new WurflException("cannot parse WURFL");
        } catch (IOException ioe) {
            System.err.println("problems reading the stream");
            ioe.printStackTrace();
            throw new WurflException("problems reading the stream");
        }
    }

    /**
     * Constructor with two InputStreams
     * 
     * @param wurflStream
     * @param patchStream
     * 
     */

    public Wurfl(InputStream wurflStream, InputStream patchStream) {
        init(wurflStream, patchStream);
    }

    /**
     * Constructor with two Files representing WURFL and patch
     * 
     * @param wurfl
     * @param patch
     */
    public Wurfl(File wurflFile, File patchFile) {
        try {
            InputStream wurfl = new FileInputStream(wurflFile);
            InputStream patch = new FileInputStream(patchFile);
            init(wurfl, patch);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new WurflException(e.getMessage());
        }
    }

    /**
     * Constructor with two Strings representing locations of WURFL and patch
     * 
     * @param fileName
     * @param patchfile
     * 
     */
    public Wurfl(String fileName, String patchfile) {
        try {
            // in case patchfile is empty, have a look if wurfl_patch.xml
            // is in the same directory as wurfl.xml
            if (patchfile == null || patchfile.equals("")) {
                log.info("trying to see if we can figure out the patch file");
                if (fileName.matches("(.*)wurfl\\.xml$")) {
                    int fc = fileName.indexOf("wurfl.xml");
                    String dir = fileName.substring(0, fc);
                    String _patchfile = dir + "wurfl_patch.xml";

                    File f = new File(_patchfile);
                    if (f.exists() && f.canRead()) {
                        log.info("potential patchfile: " + _patchfile);
                        patchfile = _patchfile;
                    } else {
                        log.info("potential patchfile: " + _patchfile + " does not exist, or is not readable");
                    }
                }
            }
            InputStream wurfl = new FileInputStream(new File(fileName));
            InputStream patch = null;
            if (patchfile != null && !patchfile.equals("")) {
                patch = new FileInputStream(new File(patchfile));
            }
            init(wurfl, patch);

        } catch (Exception e) {
            log.error("Unable to prepare WURFL", e);
        }
    }

    /**
     * Constructor taking a single filename
     * 
     * @param filename
     */
    public Wurfl(String filename) {
        this(filename, "");
    }

    // some utilities
    boolean isDeviceIn(String devID) {
        return deviceIdSet.contains(devID);
    }

    boolean isCapabilityIn(String capaName) {
        return setOfCapabilityNames.contains(capaName);
    }

    String getFallBackForDevice(String devID) {

        if (!isDeviceIn(devID)) {
            log.error("ERROR: device *" + devID + "* does not exist in WURFL");
            return "generic";
        } else {
            if (devID.equals("generic")) {
                return "generic";
            } else {
                String fb_str = ((Element) deviceElementsList.get(devID)).getAttributeValue("fall_back");
                if (fb_str == null || fb_str.equals("")) {
                    log.error("ERROR: wurfl data is not valid. \"fall_back\" for device *" + devID
                            + "* non-existent or not found.");
                    return "generic";
                }
                return fb_str;
            }
        }
    }

    /*
     * Added to make Tom Hume from Future Platforms happy My suggestion is to use
     * capabilities and patch file for operations that one thinks might require a
     * query of the WURFL hierarchy
     */
    boolean isDescendentOf(String descendent, String ancestor) {
        String fb = getFallBackForDevice(descendent);
        if (fb.equals(ancestor)) {
            return true;
        } else {
            if (fb.equals("generic") || fb.equals("")) {
                return false;
            }
            return isDescendentOf(fb, ancestor);
        }
    }

    ArrayList<String> getFallBackPathToRoot(String devID) {

        ArrayList<String> al = new ArrayList<String>(10);
        if (devID.equals("generic")) {
            al.add("generic");
            return al;
        }

        if (!isDeviceIn(devID)) {
            log.error("ERROR: Device ID " + devID + " not defined in WURFL (getFallBackPathToRoot()).");
            return al;
        }

        String looper = devID;
        while (!looper.equals("generic")) {
            al.add(looper);
            looper = getFallBackForDevice(looper);
        }
        al.add("generic");
        return al;
    }

    boolean isCapabilityDefinedInDevice(String devID, String capaName) {

        if (!isDeviceIn(devID)) {
            log.error("ERROR: Device ID " + devID + " not defined in WURFL.(isCapabilityDefinedInDevice())");
            return false;
        }
        if (!isCapabilityIn(capaName)) {
            log.error("ERROR: capability " + capaName + " not defined in WURFL.(isCapabilityDefinedInDevice())");
            return false;
        }

        Element el = (Element) deviceElementsList.get(devID);
        Elements groups = el.getChildElements("group");
        if (groups.size() == 0) {
            return false;
        }

        Elements capaList = null;
        for (int i = 0; i < groups.size(); i++) {
            capaList = groups.get(i).getChildElements("capability");
            if (capaList.size() == 0) {
                continue;
            }
            for (int j = 0; j < capaList.size(); j++) {
                if (capaList.get(j).getAttributeValue("name").equals(capaName)) {
                    return true;
                }
            }
        }
        return false;
    }

    String getDeviceWhereCapabilityIsDefined(String devID, String capaName) {

        if (!isDeviceIn(devID)) {
            log.error("ERROR: Device ID " + devID + " not defined in WURFL.(getDeviceWhereCapabilityIsDefined())");
            return "";
        }
        if (!isCapabilityIn(capaName)) {
            log.error("ERROR: capability " + capaName
                    + " not defined in WURFL.(getDeviceWhereCapabilityIsDefined())");
            return "";
        }

        if (isCapabilityDefinedInDevice(devID, capaName)) {
            return devID;
        }

        // I know the application assumes that the WURFL is valid,
        // but since a little glitch can go a long way, we do what we can to avoid
        // infinite loops
        String looper = devID;
        int i = 0;
        ArrayList<String> fb_path = new ArrayList<String>();
        while (!isCapabilityDefinedInDevice(looper, capaName)) {
            looper = getFallBackForDevice(looper);
            i++;
            fb_path.add(looper);
            if (i > 100) {
                log.error("ERROR: WURFL file is probably not valid. Infinite-loop detected:");
                log.error("loop of device IDs: " + fb_path.toString());
                return "generic";
            }
        }

        return looper; // of course, it assumes that generic has all the
        // capabilities
    }

    String getCapabilityValueForDeviceAndCapability(String devID, String capaName) {

        if (!isDeviceIn(devID)) {
            log.error("ERROR: Device ID " + devID + " not defined in WURFL.(isCapabilityDefinedInDevice())");
            return "";
        }
        if (!isCapabilityIn(capaName)) {
            log.error("ERROR: capability " + capaName + " not defined in WURFL.(isCapabilityDefinedInDevice())");
            return "";
        }

        // this is just to speed things up
        if (devID.equals("generic")) {
            return (String) genericCapabilityNameValues.get(capaName);
        }

        String devWithCapabilityDefinition = getDeviceWhereCapabilityIsDefined(devID, capaName);
        Element el = (Element) deviceElementsList.get(devWithCapabilityDefinition);

        // we need to scan all the capas
        Elements groups = el.getChildElements("group");
        Elements capaList = null;
        for (int i = 0; i < groups.size(); i++) {
            capaList = groups.get(i).getChildElements("capability");
            if (capaList.size() == 0) {
                continue;
            } // necessary?
            for (int j = 0; j < capaList.size(); j++) {
                if (capaList.get(j).getAttributeValue("name").equals(capaName)) {
                    return capaList.get(j).getAttributeValue("value");
                }
            }
        }

        // we should never get there if WURFL is correct
        System.out.println(
                "WURFL is not VALID! Unfortunately, the library was unable to determine the origin of the error.");
        return "";
    }

    // Retrieve Device ID through UA (strict: only exact matches)
    public String getDeviceIDFromUA(String ua) {

        String devID = listOfUAWithDeviceID.get(ua);
        if (devID == null) {
            return "generic";
        } else {
            return devID;
        }
    }

    // Retrieve Device ID through UA (loose: longest match)
    // for ex: "MOT-T720/G_05.01.43R" will match
    // "MOT-T720/G_05.01.43R MIB/2.0 Profile/MIDP-1.0 Configuration/CLDC-1.0"
    // and the "mot_t720_ver1_subg050143r" Device ID will be returned
    // This is more than a substring match. The UA string is made
    // progressively shorter and a new match is attempted each time.
    // The purpose is to match devices which are not available in the WURFL
    // but may have user agents (and capabilities) similar to the ones
    // of WURFL devices.
    public String getDeviceIDFromUALoose(String ua) {

        String key;

        if (ua.length() == 0) {
            return "generic";
        }

        // giving it a try ;)
        Object strictUA = listOfUAWithDeviceID.get(ua);
        if (strictUA != null) {
            return (String) strictUA;
        }

        String uaSub = ua;

        // let's try to match on progressively shorter UAs
        while (uaSub.length() > 5) {
            Iterator<String> keys = listOfUAWithDeviceID.keySet().iterator();
            while (keys.hasNext()) {
                key = (String) keys.next();
                if (key.indexOf(uaSub) != -1) {
                    return (String) listOfUAWithDeviceID.get(key);
                }
            }
            uaSub = uaSub.substring(0, uaSub.length() - 1);
        }

        // Nasty habit by Vodafone to add their f***ing brand
        // everwhere. Fix it and make sure that sys-admins know that
        // Voda sucks
        if (ua.startsWith("Vodafone/")) {
            log.info("Voda added their f***ing brand to the " + "following user-agent string: " + ua);
            // log.error("***** Vodafone sucks! *******");
            String clean_ua = ua.substring(9, ua.length());
            String temp_res = getDeviceIDFromUALoose(clean_ua);
            if (!"generic".equals(temp_res)) {
                return temp_res;
            }
        }

        // before we give up and return generic, one last
        // attempt to catch well-behaved Nokia and Openwave browsers!
        if (ua.indexOf("UP.Browser/7") != -1) {
            return "opwv_v7_generic";
        }
        if (ua.indexOf("UP.Browser/6") != -1) {
            return "opwv_v6_generic";
        }
        if (ua.indexOf("UP.Browser/5") != -1) {
            return "upgui_generic";
        }
        if (ua.indexOf("UP.Browser/4") != -1) {
            return "uptext_generic";
        }
        if (ua.indexOf("Series60") != -1) {
            return "nokia_generic_series60";
        }
        // web browsers?
        if (ua.indexOf("Mozilla/4.0") != -1) {
            return "generic_web_browser";
        }
        if (ua.indexOf("Mozilla/5.0") != -1) {
            return "generic_web_browser";
        }
        if (ua.indexOf("Mozilla/6.0") != -1) {
            return "generic_web_browser";
        }
        return "generic";
    }

    // patch utility methods
    private void patch_parse_error(String msg) {
        log.error("Fatal error in parsing wurfl patch file: " + msg);
        throw new WurflException(msg);
    }

    private void merge_group_capabilities(Element wurfl_el, Element patch_el) {

        Elements patch_capas = patch_el.getChildElements("capability");
        Elements wurfl_capas = wurfl_el.getChildElements("capability");
        for (int k = 0; k < patch_capas.size(); k++) {
            Element patch_capa = patch_capas.get(k);
            String patch_capaname = patch_capa.getAttributeValue("name");
            boolean found = false;
            Element wurfl_capa = null;
            for (int i = 0; i < wurfl_capas.size(); i++) {
                wurfl_capa = wurfl_capas.get(i);
                String wurfl_capaname = wurfl_capa.getAttributeValue("name");
                if (wurfl_capaname.equals(patch_capaname)) {
                    log.debug("Found " + wurfl_capaname + " in both groups " + wurfl_el.getAttributeValue("id"));
                    found = true;
                    break;
                }
            }
            if (found) {
                // capa exists from before. Remove and replace with one in patch
                log.debug("need to replace...remove " + wurfl_capa.toXML());
                wurfl_el.removeChild(wurfl_capa);
                log.debug("add " + patch_capa.toXML());
                wurfl_el.appendChild(patch_capa.copy());
            } else {
                // capa not found. Simply append
                log.debug("No need to replace. Just add " + patch_capa.toXML());
                wurfl_el.appendChild(patch_capa.copy());
            }

        }
    }

    public String toXML() {
        if (wurflDocument != null) {
            try {
                return toPrettyXML(wurflDocument);
            } catch (Exception e) {
                return "<sorry>WURFL has not been parsed yet!</sorry>";
            }
        } else {
            return "<sorry>WURFL has not been parsed yet!</sorry>";
        }
    }

    public static void toPrettyXML(Document doc, OutputStream out) throws Exception {
        Serializer serializer = new Serializer(out);
        serializer.setIndent(2);
        serializer.setMaxLength(200);
        serializer.setPreserveBaseURI(false);
        serializer.write(doc);
        serializer.flush();
        out.close();
    }

    public static String toPrettyXML(Document doc) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        toPrettyXML(doc, out);
        return out.toString();
    }

    /* Filter out capabilities that are not required */
    public void filterCapabilities(HashSet<String> capaAcceptedList, OutputStream myout) {

        if (wurflDocument == null) {
            return;
        }
        Document tmpWurflDocument = (Document) wurflDocument.copy();
        // cleaning. remove version element
        Element version = tmpWurflDocument.getRootElement().getFirstChildElement("version");
        if (version != null) {
            tmpWurflDocument.getRootElement().removeChild(version);
        }

        Element devices_elem = tmpWurflDocument.getRootElement().getFirstChildElement("devices");

        // more cleaning. remove all comments
        for (int h = 0; h < devices_elem.getChildCount(); h++) {
            Node child = devices_elem.getChild(h);
            if (child instanceof Comment) {
                devices_elem.removeChild(child);
            }
        }

        Elements devices = devices_elem.getChildElements("device");
        // int numberOfDevices = devices.size();

        for (int k = 0; k < devices.size(); k++) {
            Element device = devices.get(k);
            // some cleaning. remove all comments
            for (int h = 0; h < device.getChildCount(); h++) {
                Node child = device.getChild(h);
                if (child instanceof Comment) {
                    device.removeChild(child);
                }
            }
            Elements groups = device.getChildElements("group");

            // extra list to keep capabilities grouped by group
            for (int i = 0; i < groups.size(); i++) {
                Element group = groups.get(i);

                // some cleaning. remove all comments
                for (int h = 0; h < group.getChildCount(); h++) {
                    Node child = group.getChild(h);
                    if (child instanceof Comment) {
                        group.removeChild(child);
                    }
                }
                Elements capaList = group.getChildElements("capability");
                for (int j = 0; j < capaList.size(); j++) {
                    Element capa = capaList.get(j);
                    String capa_name = capa.getAttributeValue("name");
                    if (!capaAcceptedList.contains(capa_name)) {
                        // remove capability
                        group.removeChild(capa);
                    }
                }
                // if group has no capability, remove group too
                if (0 == group.getChildElements("capability").size()) {
                    device.removeChild(group);
                }
            }
        }
        try {
            toPrettyXML(tmpWurflDocument, myout);
        } catch (Exception e) {
            new WurflException("Error producing filtered WURFL:" + e.getMessage());
        }
    }

    // GETTERs list

    /**
     * Returns the deviceIdSet.
     * 
     * @return HashSet
     */
    public HashSet<String> getDeviceIdSet() {
        return deviceIdSet;
    }

    /**
     * Returns the listOfUAWithDevice.
     * 
     * @return HashMap
     */

    public HashMap<String, String> getListOfUAWithDeviceID() {
        return listOfUAWithDeviceID;
    }

    /**
     * Returns the numberOfDevices.
     * 
     * @return int
     */
    public int getNumberOfDevices() {
        return numberOfDevices;
    }

    /**
     * Returns the deviceElementsList.
     * 
     * @return HashMap
     */
    public HashMap<String, Element> getDeviceElementsList() {
        return deviceElementsList;
    }

    /**
     * Returns the actualDeviceElementsList.
     * 
     * @return TreeMap
     */
    public TreeMap<String, Element> getActualDeviceElementsList() {

        if (actualDeviceElementsList == null) {

            actualDeviceElementsList = new TreeMap<String, Element>();
            // we need to find the list of devices which represent actual devices
            Iterator<String> keys = deviceElementsList.keySet().iterator();
            while (keys.hasNext()) {
                String key = keys.next();
                Element el = deviceElementsList.get(key);
                String act_dev = el.getAttributeValue("actual_device_root");
                if (act_dev != null && act_dev.equals("true")) {
                    log.debug("Trovato device: " + key);
                    actualDeviceElementsList.put(key, el);
                }
            }
        }
        return actualDeviceElementsList;
    }

    /**
     * Returns the listOfCapabilities.
     * 
     * @return ArrayList
     */
    public ArrayList<String> getListOfCapabilities() {
        return listOfCapabilities;
    }

    /**
     * Returns the numberOfCapabilities.
     * 
     * @return int
     */
    public int getNumberOfCapabilities() {
        return numberOfCapabilities;
    }

    /**
     * Returns HashMap of ArrayList (capabilities grouped by group name (id)).
     * 
     * @return HashMap
     */
    public HashMap<String, ArrayList<String>> getListOfGroups() {
        return listOfGroups;
    }

    /**
     * Returns the devices.
     * 
     * @return Elements
     */
    public Elements getDevices() {
        return devices;
    }

    /**
     * Returns the setOfCapabilityNames.
     * 
     * @return HashSet
     */
    public HashSet<String> getSetOfCapabilityNames() {
        return setOfCapabilityNames;
    }

}