<commit_before>/**
 * This code is free software; you can redistribute it and/or modify it under
 * the terms of the new BSD License.
 *
 * Copyright (c) 2008-2011, <NAME>
 */

package com.github.koraktor.steamcondenser.steam.community;

import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.lang3.StringEscapeUtils;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.github.koraktor.steamcondenser.exceptions.SteamCondenserException;

/**
 * The SteamId class represents a Steam Community profile (also called Steam
 * ID)
 *
 * @author <NAME>
 */
public class SteamId {

    private static Map<Object, SteamId> steamIds = new HashMap<Object, SteamId>();

    private String customUrl;
    private String favoriteGame;
    private float favoriteGameHoursPlayed;
    private long fetchTime;
    private SteamId[] friends;
    private HashMap<Integer, SteamGame> games;
    private SteamGroup[] groups;
    private String headLine;
    private float hoursPlayed;
    private String imageUrl;
    private Map<String, String> links;
    private String location;
    private Date memberSince;
    private Map<String, Float> mostPlayedGames;
    private String onlineState;
    private Map<Integer, int[]> playtimes;
    private String privacyState;
    private String realName;
    private String stateMessage;
    private String nickname;
    private long steamId64;
    private float steamRating;
    private String summary;
    private boolean vacBanned;
    private int visibilityState;

    /**
     * Converts a 64bit numeric SteamID as used by the Steam Community to a
     * SteamID as reported by game servers
     *
     * @param communityId The SteamID string as used by the Steam Community
     * @return The converted SteamID, like <code>STEAM_0:0:12345</code>
     * @throws SteamCondenserException if the community ID is to small
     */
    public static String convertCommunityIdToSteamId(long communityId)
            throws SteamCondenserException {
        long steamId1 = communityId % 2;
        long steamId2 = communityId - 76561197960265728L;

        if(steamId2 <= 0) {
            throw new SteamCondenserException("SteamID " + communityId + " is too small.");
        }

        steamId2 = (steamId2 - steamId1) / 2;

        return "STEAM_0:" + steamId1 + ":" + steamId2;
    }

    /**
     * Converts a SteamID as reported by game servers to a 64bit numeric
     * SteamID as used by the Steam Community
     *
     * @param steamId The SteamID string as used on servers, like
     *        <code>STEAM_0:0:12345</code>
     * @return The converted 64bit numeric SteamID
     * @throws SteamCondenserException if the SteamID doesn't have the correct
     *         format
     */
    public static long convertSteamIdToCommunityId(String steamId)
            throws SteamCondenserException {
        if(steamId.equals("STEAM_ID_LAN") || steamId.equals("BOT")) {
            throw new SteamCondenserException("Cannot convert SteamID \"" + steamId + "\" to a community ID.");
        }
        if(!steamId.matches("^STEAM_[0-1]:[0-1]:[0-9]+$")) {
            throw new SteamCondenserException("SteamID \"" + steamId + "\" doesn't have the correct format.");
        }

        String[] tmpId = steamId.substring(6).split(":");

        return Long.valueOf(tmpId[1]) + Long.valueOf(tmpId[2]) * 2 + 76561197960265728L;
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(long id)
            throws SteamCondenserException {
        return SteamId.create((Object) id, true, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(String id)
            throws SteamCondenserException {
        return SteamId.create((Object) id, true, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(long id, boolean fetch)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(String id, boolean fetch)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @param bypassCache If <code>true</code> an already cached instance for
     *        this Steam ID will be ignored and a new one will be created
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(long id, boolean fetch, boolean bypassCache)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, bypassCache);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The custom URL of the Steam ID specified by player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @param bypassCache If <code>true</code> an already cached instance for
     *        this Steam ID will be ignored and a new one will be created
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(String id, boolean fetch, boolean bypassCache)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, bypassCache);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The custom URL of the Steam ID specified by player or the 64bit
     *        SteamID
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @param bypassCache If <code>true</code> an already cached instance for
     *        this Steam ID will be ignored and a new one will be created
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    private static SteamId create(Object id, boolean fetch, boolean bypassCache)
            throws SteamCondenserException {
        if(SteamId.isCached(id) && !bypassCache) {
            SteamId steamId = SteamId.steamIds.get(id);
            if(fetch && !steamId.isFetched()) {
                steamId.fetchData();
            }
            return steamId;
        } else {
            return new SteamId(id, fetch);
        }
    }

    /**
     * Returns whether the requested Steam ID is already cached
     *
     * @param id The custom URL of the Steam ID specified by the player or the
     *        64bit SteamID
     * @return <code>true</code> if this Steam ID is already cached
     */
    public static boolean isCached(Object id) {
        return SteamId.steamIds.containsKey(id);
    }

    /**
     * Creates a new <code>SteamId</code> instance for the given ID
     *
     * @param id The custom URL of the group specified by the player or the
     *        64bit SteamID
     * @param fetchData if <code>true</code> the profile's data is loaded into
     *        the object
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    private SteamId(Object id, boolean fetchData)
            throws SteamCondenserException {
        if(id instanceof String) {
            this.customUrl = (String) id;
        } else {
            this.steamId64 = (Long) id;
        }

        if(fetchData) {
            this.fetchData();
        }

        this.cache();
    }

    /**
     * Saves this <code>SteamId</code> instance in the cache
     *
     * @return <code>false</code> if this group is already cached
     */
    public boolean cache() {
        if(!SteamId.steamIds.containsKey(this.steamId64)) {
            SteamId.steamIds.put(this.steamId64, this);
            if(this.customUrl != null && !SteamId.steamIds.containsKey(this.customUrl)) {
                SteamId.steamIds.put(this.customUrl, this);
            }
            return true;
        }
        return false;
    }
    
    /**
     * Get the data from the XML and gives a way for a nicer exception messages.
     * 
     * TODO: rafactoring extract the method to a separe class
     * 
     * @param document the w3c dom document to be looked
     * @param elementId the name of the dom node
     * @return the content of a specific node on the dom document
     */
    private static String getData(Element document, String elementId) {
    		return document.getElementsByTagName(elementId).item(0).getTextContent();
    }

    /**
     * Fetchs data from the Steam Community by querying the XML version of the
     * profile specified by the ID of this Steam ID
     *
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private, or when it cannot be parsed
     */
    public void fetchData() throws SteamCondenserException {
        try {
            String url = this.getBaseUrl() + "?xml=1";
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Element profile = parser.parse(url).getDocumentElement();

            if(profile.getElementsByTagName("error").getLength() > 0) {
                throw new SteamCondenserException(getData(profile, "error"));
            }

            this.nickname  = StringEscapeUtils.unescapeXml(getData(profile, "steamID"));
            this.steamId64 = Long.parseLong(getData(profile, "steamID64"));
            this.vacBanned = (getData(profile, "vacBanned").equals("1"));

            if(profile.getElementsByTagName("privacyMessage").getLength() > 0) {
                throw new SteamCondenserException(getData(profile, "privacyMessage"));
            }

            String avatarIconUrl = getData(profile, "avatarIcon");
            this.imageUrl = avatarIconUrl.substring(0, avatarIconUrl.length() - 4);
            this.onlineState = getData(profile, "onlineState");
            this.privacyState = getData(profile, "privacyState");
            this.stateMessage = getData(profile, "stateMessage");
            this.visibilityState = Integer.parseInt(getData(profile, "visibilityState"));

            if(this.privacyState.compareTo("public") == 0) {
                this.customUrl = getData(profile, "customURL");
                if(this.customUrl.length() == 0) {
                    this.customUrl = null;
                }

                Element favoriteGame = (Element) profile.getElementsByTagName("favoriteGame").item(0);
                if(favoriteGame != null) {
                    this.favoriteGame = getData(favoriteGame, "name");
                    this.favoriteGameHoursPlayed = Float.parseFloat(getData(favoriteGame, "hoursPlayed2wk"));
                }
                this.headLine = StringEscapeUtils.unescapeXml(getData(profile, "headline"));
                this.hoursPlayed = Float.parseFloat(getData(profile, "hoursPlayed2Wk"));
                this.location = getData(profile, "location");
                this.memberSince = DateFormat.getDateInstance(DateFormat.LONG,Locale.ENGLISH).parse(getData(profile, "memberSince"));
                this.realName = StringEscapeUtils.unescapeXml(getData(profile, "realname"));
                this.steamRating = Float.parseFloat(getData(profile, "steamRating"));
                this.summary = StringEscapeUtils.unescapeXml(getData(profile, "summary"));

                this.mostPlayedGames = new HashMap<String, Float>();
                Element mostPlayedGamesNode = (Element) profile.getElementsByTagName("mostPlayedGames").item(0);
                if(mostPlayedGamesNode != null) {
                    NodeList mostPlayedGameList = mostPlayedGamesNode.getElementsByTagName("mostPlayedGame");
                    for(int i = 0; i < mostPlayedGameList.getLength(); i++) {
                        Element mostPlayedGame = (Element) mostPlayedGameList.item(i);
                        this.mostPlayedGames.put(getData(mostPlayedGame, "gameName"), Float.parseFloat(getData(mostPlayedGame, "hoursPlayed")));
                    }
                }

                Element groupsNode = (Element) profile.getElementsByTagName(
                        "groups").item(0);
                if(groupsNode != null) {
                    NodeList groupsNodeList = groupsNode.getElementsByTagName("group");
                    this.groups = new SteamGroup[groupsNodeList.getLength()];
                    for(int i = 0; i < groupsNodeList.getLength(); i++) {
                        Element group = (Element) groupsNodeList.item(i);
                        this.groups[i] = SteamGroup.create(Long.parseLong(getData(group, "groupID64")), false);
                    }
                }

                this.links = new HashMap<String, String>();
                Element weblinksNode = (Element) profile.getElementsByTagName("weblinks").item(0);
                if(weblinksNode != null) {
                    NodeList weblinksList = weblinksNode.getElementsByTagName("weblink");
                    for(int i = 0; i < weblinksList.getLength(); i++) {
                        Element weblink = (Element) weblinksList.item(i);
                        this.links.put(StringEscapeUtils.unescapeXml(getData(weblink, "title")), getData(weblink, "link"));
                    }
                }
            }
        } catch(Exception e) {
            throw new SteamCondenserException("XML data could not be parsed.", e);
        }

        this.fetchTime = new Date().getTime();
    }

    /**
     * Fetches the friends of this user
     * <p>
     * This creates a new <code>SteamId</code> instance for each of the friends
     * without fetching their data.
     *
     * @see #getFriends
     * @see SteamId#SteamId
     * @throws SteamCondenserException if an error occurs while parsing the
     *         data
     */
    private void fetchFriends() throws SteamCondenserException {
        try {
            String url = this.getBaseUrl() + "/friends?xml=1";
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Element friendsData = parser.parse(url).getDocumentElement();

            Element friendsNode = (Element) friendsData.getElementsByTagName("friends").item(0);
            NodeList friendsNodeList = friendsNode.getElementsByTagName("friend");
            this.friends = new SteamId[friendsNodeList.getLength()];
            for(int i = 0; i < friendsNodeList.getLength(); i++) {
                Element friend = (Element) friendsNodeList.item(i);
                this.friends[i] = SteamId.create(Long.parseLong(friend.getTextContent()), false);
            }
        } catch(Exception e) {
            throw new SteamCondenserException("XML data could not be parsed.", e);
        }
    }

    /**
     * Fetches the games this user owns
     *
     * @see #getGames
     * @throws SteamCondenserException if an error occurs while parsing the
     *         data
     */
    private void fetchGames() throws SteamCondenserException {
        try {
            String url = this.getBaseUrl() + "/games?xml=1";
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Element gamesData = parser.parse(url).getDocumentElement();

            Element gamesNode = (Element) gamesData.getElementsByTagName("games").item(0);
            NodeList gamesNodeList = gamesNode.getElementsByTagName("game");
            this.games = new HashMap<Integer, SteamGame>();
            this.playtimes = new HashMap<Integer, int[]>();
            for(int i = 0; i < gamesNodeList.getLength(); i++) {
                Element gameData = (Element) gamesNodeList.item(i);
                SteamGame game = SteamGame.create(gameData);
                this.games.put(game.getAppId(), game);
                float recent;
                try {
                    recent = Float.parseFloat(getData(gameData, "hoursLast2Weeks"));
                } catch(NullPointerException e) {
                    recent = 0;
                }
                float total;
                try {
                    total = Float.parseFloat(getData(gameData, "hoursOnRecord"));
                } catch(NullPointerException e) {
                    total = 0;
                }<commit_msg>Added a better handle to invalid dom elements at getData method<commit_after>/**
 * This code is free software; you can redistribute it and/or modify it under
 * the terms of the new BSD License.
 *
 * Copyright (c) 2008-2011, <NAME>
 */

package com.github.koraktor.steamcondenser.steam.community;

import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.lang3.StringEscapeUtils;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.github.koraktor.steamcondenser.exceptions.SteamCondenserException;

/**
 * The SteamId class represents a Steam Community profile (also called Steam
 * ID)
 *
 * @author <NAME>
 */
public class SteamId {

    private static Map<Object, SteamId> steamIds = new HashMap<Object, SteamId>();

    private String customUrl;
    private String favoriteGame;
    private float favoriteGameHoursPlayed;
    private long fetchTime;
    private SteamId[] friends;
    private HashMap<Integer, SteamGame> games;
    private SteamGroup[] groups;
    private String headLine;
    private float hoursPlayed;
    private String imageUrl;
    private Map<String, String> links;
    private String location;
    private Date memberSince;
    private Map<String, Float> mostPlayedGames;
    private String onlineState;
    private Map<Integer, int[]> playtimes;
    private String privacyState;
    private String realName;
    private String stateMessage;
    private String nickname;
    private long steamId64;
    private float steamRating;
    private String summary;
    private boolean vacBanned;
    private int visibilityState;

    /**
     * Converts a 64bit numeric SteamID as used by the Steam Community to a
     * SteamID as reported by game servers
     *
     * @param communityId The SteamID string as used by the Steam Community
     * @return The converted SteamID, like <code>STEAM_0:0:12345</code>
     * @throws SteamCondenserException if the community ID is to small
     */
    public static String convertCommunityIdToSteamId(long communityId)
            throws SteamCondenserException {
        long steamId1 = communityId % 2;
        long steamId2 = communityId - 76561197960265728L;

        if(steamId2 <= 0) {
            throw new SteamCondenserException("SteamID " + communityId + " is too small.");
        }

        steamId2 = (steamId2 - steamId1) / 2;

        return "STEAM_0:" + steamId1 + ":" + steamId2;
    }

    /**
     * Converts a SteamID as reported by game servers to a 64bit numeric
     * SteamID as used by the Steam Community
     *
     * @param steamId The SteamID string as used on servers, like
     *        <code>STEAM_0:0:12345</code>
     * @return The converted 64bit numeric SteamID
     * @throws SteamCondenserException if the SteamID doesn't have the correct
     *         format
     */
    public static long convertSteamIdToCommunityId(String steamId)
            throws SteamCondenserException {
        if(steamId.equals("STEAM_ID_LAN") || steamId.equals("BOT")) {
            throw new SteamCondenserException("Cannot convert SteamID \"" + steamId + "\" to a community ID.");
        }
        if(!steamId.matches("^STEAM_[0-1]:[0-1]:[0-9]+$")) {
            throw new SteamCondenserException("SteamID \"" + steamId + "\" doesn't have the correct format.");
        }

        String[] tmpId = steamId.substring(6).split(":");

        return Long.valueOf(tmpId[1]) + Long.valueOf(tmpId[2]) * 2 + 76561197960265728L;
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(long id)
            throws SteamCondenserException {
        return SteamId.create((Object) id, true, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(String id)
            throws SteamCondenserException {
        return SteamId.create((Object) id, true, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(long id, boolean fetch)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(String id, boolean fetch)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, false);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The 64bit SteamID of the player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @param bypassCache If <code>true</code> an already cached instance for
     *        this Steam ID will be ignored and a new one will be created
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(long id, boolean fetch, boolean bypassCache)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, bypassCache);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The custom URL of the Steam ID specified by player
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @param bypassCache If <code>true</code> an already cached instance for
     *        this Steam ID will be ignored and a new one will be created
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    public static SteamId create(String id, boolean fetch, boolean bypassCache)
            throws SteamCondenserException {
        return SteamId.create((Object) id, fetch, bypassCache);
    }

    /**
     * Creates a new <code>SteamID</code> instance or gets an existing one
     * from the cache for the profile with the given ID
     *
     * @param id The custom URL of the Steam ID specified by player or the 64bit
     *        SteamID
     * @param fetch if <code>true</code> the profile's data is loaded into the
     *        object
     * @param bypassCache If <code>true</code> an already cached instance for
     *        this Steam ID will be ignored and a new one will be created
     * @return The <code>SteamId</code> instance of the requested profile
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    private static SteamId create(Object id, boolean fetch, boolean bypassCache)
            throws SteamCondenserException {
        if(SteamId.isCached(id) && !bypassCache) {
            SteamId steamId = SteamId.steamIds.get(id);
            if(fetch && !steamId.isFetched()) {
                steamId.fetchData();
            }
            return steamId;
        } else {
            return new SteamId(id, fetch);
        }
    }

    /**
     * Returns whether the requested Steam ID is already cached
     *
     * @param id The custom URL of the Steam ID specified by the player or the
     *        64bit SteamID
     * @return <code>true</code> if this Steam ID is already cached
     */
    public static boolean isCached(Object id) {
        return SteamId.steamIds.containsKey(id);
    }

    /**
     * Creates a new <code>SteamId</code> instance for the given ID
     *
     * @param id The custom URL of the group specified by the player or the
     *        64bit SteamID
     * @param fetchData if <code>true</code> the profile's data is loaded into
     *        the object
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private
     */
    private SteamId(Object id, boolean fetchData)
            throws SteamCondenserException {
        if(id instanceof String) {
            this.customUrl = (String) id;
        } else {
            this.steamId64 = (Long) id;
        }

        if(fetchData) {
            this.fetchData();
        }

        this.cache();
    }

    /**
     * Saves this <code>SteamId</code> instance in the cache
     *
     * @return <code>false</code> if this group is already cached
     */
    public boolean cache() {
        if(!SteamId.steamIds.containsKey(this.steamId64)) {
            SteamId.steamIds.put(this.steamId64, this);
            if(this.customUrl != null && !SteamId.steamIds.containsKey(this.customUrl)) {
                SteamId.steamIds.put(this.customUrl, this);
            }
            return true;
        }
        return false;
    }
    
    /**
     * Get the data from the XML and gives a way for a nicer exception messages.
     * 
     * TODO: rafactoring extract the method to a separe class
     * 
     * @param document the w3c dom document to be looked
     * @param elementId the name of the dom node
     * @return the content of a specific node on the dom document
     */
    private static String getData(Element document, String elementId) throws SteamCondenserException{
		try {
			return document.getElementsByTagName(elementId).item(0)
					.getTextContent();
		} catch (Exception e) {
			throw new SteamCondenserException("Error with "+elementId+" element");
		}
    }

    /**
     * Fetchs data from the Steam Community by querying the XML version of the
     * profile specified by the ID of this Steam ID
     *
     * @throws SteamCondenserException if the Steam ID data is not available,
     *         e.g. when it is private, or when it cannot be parsed
     */
    public void fetchData() throws SteamCondenserException {
        try {
            String url = this.getBaseUrl() + "?xml=1";
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Element profile = parser.parse(url).getDocumentElement();

            if(profile.getElementsByTagName("error").getLength() > 0) {
                throw new SteamCondenserException(getData(profile, "error"));
            }

            this.nickname  = StringEscapeUtils.unescapeXml(getData(profile, "steamID"));
            this.steamId64 = Long.parseLong(getData(profile, "steamID64"));
            this.vacBanned = (getData(profile, "vacBanned").equals("1"));

            if(profile.getElementsByTagName("privacyMessage").getLength() > 0) {
                throw new SteamCondenserException(getData(profile, "privacyMessage"));
            }

            String avatarIconUrl = getData(profile, "avatarIcon");
            this.imageUrl = avatarIconUrl.substring(0, avatarIconUrl.length() - 4);
            this.onlineState = getData(profile, "onlineState");
            this.privacyState = getData(profile, "privacyState");
            this.stateMessage = getData(profile, "stateMessage");
            this.visibilityState = Integer.parseInt(getData(profile, "visibilityState"));

            if(this.privacyState.compareTo("public") == 0) {
                this.customUrl = getData(profile, "customURL");
                if(this.customUrl.length() == 0) {
                    this.customUrl = null;
                }

                Element favoriteGame = (Element) profile.getElementsByTagName("favoriteGame").item(0);
                if(favoriteGame != null) {
                    this.favoriteGame = getData(favoriteGame, "name");
                    this.favoriteGameHoursPlayed = Float.parseFloat(getData(favoriteGame, "hoursPlayed2wk"));
                }
                this.headLine = StringEscapeUtils.unescapeXml(getData(profile, "headline"));
                this.hoursPlayed = Float.parseFloat(getData(profile, "hoursPlayed2Wk"));
                this.location = getData(profile, "location");
                this.memberSince = DateFormat.getDateInstance(DateFormat.LONG,Locale.ENGLISH).parse(getData(profile, "memberSince"));
                this.realName = StringEscapeUtils.unescapeXml(getData(profile, "realname"));
                this.steamRating = Float.parseFloat(getData(profile, "steamRating"));
                this.summary = StringEscapeUtils.unescapeXml(getData(profile, "summary"));

                this.mostPlayedGames = new HashMap<String, Float>();
                Element mostPlayedGamesNode = (Element) profile.getElementsByTagName("mostPlayedGames").item(0);
                if(mostPlayedGamesNode != null) {
                    NodeList mostPlayedGameList = mostPlayedGamesNode.getElementsByTagName("mostPlayedGame");
                    for(int i = 0; i < mostPlayedGameList.getLength(); i++) {
                        Element mostPlayedGame = (Element) mostPlayedGameList.item(i);
                        this.mostPlayedGames.put(getData(mostPlayedGame, "gameName"), Float.parseFloat(getData(mostPlayedGame, "hoursPlayed")));
                    }
                }

                Element groupsNode = (Element) profile.getElementsByTagName(
                        "groups").item(0);
                if(groupsNode != null) {
                    NodeList groupsNodeList = groupsNode.getElementsByTagName("group");
                    this.groups = new SteamGroup[groupsNodeList.getLength()];
                    for(int i = 0; i < groupsNodeList.getLength(); i++) {
                        Element group = (Element) groupsNodeList.item(i);
                        this.groups[i] = SteamGroup.create(Long.parseLong(getData(group, "groupID64")), false);
                    }
                }

                this.links = new HashMap<String, String>();
                Element weblinksNode = (Element) profile.getElementsByTagName("weblinks").item(0);
                if(weblinksNode != null) {
                    NodeList weblinksList = weblinksNode.getElementsByTagName("weblink");
                    for(int i = 0; i < weblinksList.getLength(); i++) {
                        Element weblink = (Element) weblinksList.item(i);
                        this.links.put(StringEscapeUtils.unescapeXml(getData(weblink, "title")), getData(weblink, "link"));
                    }
                }
            }
        } catch(Exception e) {
            throw new SteamCondenserException("XML data could not be parsed.", e);
        }

        this.fetchTime = new Date().getTime();
    }

    /**
     * Fetches the friends of this user
     * <p>
     * This creates a new <code>SteamId</code> instance for each of the friends
     * without fetching their data.
     *
     * @see #getFriends
     * @see SteamId#SteamId
     * @throws SteamCondenserException if an error occurs while parsing the
     *         data
     */
    private void fetchFriends() throws SteamCondenserException {
        try {
            String url = this.getBaseUrl() + "/friends?xml=1";
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Element friendsData = parser.parse(url).getDocumentElement();

            Element friendsNode = (Element) friendsData.getElementsByTagName("friends").item(0);
            NodeList friendsNodeList = friendsNode.getElementsByTagName("friend");
            this.friends = new SteamId[friendsNodeList.getLength()];
            for(int i = 0; i < friendsNodeList.getLength(); i++) {
                Element friend = (Element) friendsNodeList.item(i);
                this.friends[i] = SteamId.create(Long.parseLong(friend.getTextContent()), false);
            }
        } catch(Exception e) {
            throw new SteamCondenserException("XML data could not be parsed.", e);
        }
    }

    /**
     * Fetches the games this user owns
     *
     * @see #getGames
     * @throws SteamCondenserException if an error occurs while parsing the
     *         data
     */
    private void fetchGames() throws SteamCondenserException {
        try {
            String url = this.getBaseUrl() + "/games?xml=1";
            DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Element gamesData = parser.parse(url).getDocumentElement();

            Element gamesNode = (Element) gamesData.getElementsByTagName("games").item(0);
            NodeList gamesNodeList = gamesNode.getElementsByTagName("game");
            this.games = new HashMap<Integer, SteamGame>();
            this.playtimes = new HashMap<Integer, int[]>();
            for(int i = 0; i < gamesNodeList.getLength(); i++) {
                Element gameData = (Element) gamesNodeList.item(i);
                SteamGame game = SteamGame.create(gameData);
                this.games.put(game.getAppId(), game);
                float recent;
                try {
                    recent = Float.parseFloat(getData(gameData, "hoursLast2Weeks"));
                } catch(SteamCondenserException e) {
                    recent = 0;
                }
                float total;
                try {
                    total = Float.parseFloat(getData(gameData, "hoursOnRecord"));
                } catch(SteamCondenserException e) {
                    total = 0;
                }
{'fraction_non_alphanumeric': 0.09313634310312757, 'fraction_numerical': 0.0077060027103871605, 'mean_word_length': 2.319865913902611, 'pattern_counts': {'":': 4, '<': 161, '<?xml version=': 0, '>': 153, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 10}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5014951', 'n_tokens_mistral': 10500, 'n_tokens_neox': 10083, 'n_words': 3085}