net.sf.zekr.common.config.ApplicationConfig.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.zekr.common.config.ApplicationConfig.java

Source

/*
 *               In the name of Allah
 * This file is part of The Zekr Project. Use is subject to
 * license terms.
 *
 * Author:         Mohsen Saboorian
 * Start Date:     Sep 10, 2004
 */

package net.sf.zekr.common.config;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import net.sf.zekr.common.ZekrBaseException;
import net.sf.zekr.common.ZekrMessageException;
import net.sf.zekr.common.resource.IQuranLocation;
import net.sf.zekr.common.resource.QuranLocation;
import net.sf.zekr.common.resource.QuranPropertiesUtils;
import net.sf.zekr.common.runtime.ApplicationRuntime;
import net.sf.zekr.common.runtime.Naming;
import net.sf.zekr.common.util.CollectionUtils;
import net.sf.zekr.common.util.CommonUtils;
import net.sf.zekr.common.util.ConfigUtils;
import net.sf.zekr.common.util.IntallationProgressListener;
import net.sf.zekr.common.util.ZipUtils;
import net.sf.zekr.engine.addonmgr.AddOnManagerUtils;
import net.sf.zekr.engine.addonmgr.CandidateResource;
import net.sf.zekr.engine.addonmgr.InvalidResourceException;
import net.sf.zekr.engine.addonmgr.Resource;
import net.sf.zekr.engine.audio.Audio;
import net.sf.zekr.engine.audio.AudioCacheManager;
import net.sf.zekr.engine.audio.AudioData;
import net.sf.zekr.engine.audio.DefaultPlayerController;
import net.sf.zekr.engine.audio.PlayerController;
import net.sf.zekr.engine.audio.RecitationPackConverter;
import net.sf.zekr.engine.bookmark.BookmarkException;
import net.sf.zekr.engine.bookmark.BookmarkSet;
import net.sf.zekr.engine.bookmark.BookmarkSetGroup;
import net.sf.zekr.engine.common.LocalizedResource;
import net.sf.zekr.engine.language.Language;
import net.sf.zekr.engine.language.LanguageEngine;
import net.sf.zekr.engine.language.LanguagePack;
import net.sf.zekr.engine.log.Logger;
import net.sf.zekr.engine.network.NetworkController;
import net.sf.zekr.engine.page.CustomPagingData;
import net.sf.zekr.engine.page.FixedAyaPagingData;
import net.sf.zekr.engine.page.HizbQuarterPagingData;
import net.sf.zekr.engine.page.IPagingData;
import net.sf.zekr.engine.page.JuzPagingData;
import net.sf.zekr.engine.page.QuranPaging;
import net.sf.zekr.engine.page.SuraPagingData;
import net.sf.zekr.engine.revelation.Revelation;
import net.sf.zekr.engine.revelation.RevelationData;
import net.sf.zekr.engine.root.QuranRoot;
import net.sf.zekr.engine.search.SearchInfo;
import net.sf.zekr.engine.search.lucene.LuceneIndexManager;
import net.sf.zekr.engine.server.HttpServer;
import net.sf.zekr.engine.theme.Theme;
import net.sf.zekr.engine.theme.ThemeData;
import net.sf.zekr.engine.translation.Translation;
import net.sf.zekr.engine.translation.TranslationData;
import net.sf.zekr.engine.translation.TranslationException;
import net.sf.zekr.engine.xml.XmlReader;
import net.sf.zekr.engine.xml.XmlUtils;
import net.sf.zekr.ui.helper.EventProtocol;
import net.sf.zekr.ui.helper.EventUtils;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * This singleton class reads the config files by the first invocation of <code>getInstance()</code>. You can then read any option
 * by using available getter methods.
 * 
 * @author Mohsen Saboorian
 */
public class ApplicationConfig implements ConfigNaming {
    private final static Logger logger = Logger.getLogger(ApplicationConfig.class);
    private final static ResourceManager res = ResourceManager.getInstance();
    private static ApplicationConfig thisInstance;

    private XmlReader configReader;
    private LanguageEngine langEngine;
    private Language language;

    private Translation translation = new Translation();
    private Theme theme = new Theme();
    private Audio audio = new Audio();
    private Revelation revelation = new Revelation();
    private QuranPaging quranPaging = new QuranPaging();
    private ApplicationRuntime runtime;
    private IQuranLocation quranLocation;
    private PropertiesConfiguration props, searchProps;
    private BookmarkSet bookmarkSet;
    private BookmarkSetGroup bookmarkSetGroup = new BookmarkSetGroup();
    // private Thread httpServerThread;
    private IUserView userViewController;
    // private HttpServer httpServer;
    private LuceneIndexManager luceneIndexManager;
    private SearchInfo searchInfo;
    private QuranRoot quranRoot;
    private AudioCacheManager audioCacheManager;
    private PlayerController playerController, searchPlayerController;
    private NetworkController networkController;
    private KeyboardShortcut shortcut;

    private ApplicationConfig() {
        logger.info("Initializing application configurations...");

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Initializing Language Engine");
        language = Language.getInstance();

        runtime = new ApplicationRuntime();

        // language packs should be loaded before bookmarks
        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Configuration Files");
        loadConfig();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Language Packs");
        extractLangProps();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Bookmark Sets");
        loadBookmarkSetGroup();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Translation Packs");
        extractTransProps();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading View Properties");
        extractViewProps();

        // EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Initializing Audio Data");
        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Audio packs");
        extractAudioProps();
        setupAudioManager();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Revelation suraOrders");
        extractRevelOrderInfo();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Paging data");
        extractPagingDataProps();

        initNetworkController();
        /*
        if (isHttpServerEnabled()) {
           EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Start HTTP server");
        }
        startHttpServer();
         */

        // #extractPagingDataProps() should be called before this method
        initViewController();

        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading search metadata");
        initSearchInfo();

        luceneIndexManager = new LuceneIndexManager(props);

        if (isRootDatabaseEnabled()) {
            EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Quran root database");
            loadRootList();
        }

        logger.info("Application configurations initialized.");
        EventUtils.sendEvent(EventProtocol.SPLASH_PROGRESS + ":" + "Loading Application UI");
    }

    @SuppressWarnings("unchecked")
    private void initSearchInfo() {
        try {
            logger.info("Load search info...");

            File usi = new File(ApplicationPath.USER_SEARCH_INFO);
            if (!usi.exists()) {
                logger.info("User search info does not exist at " + ApplicationPath.USER_SEARCH_INFO);
                logger.info(
                        "Will make user search info with default values at " + ApplicationPath.MAIN_SEARCH_INFO);
                String searchInfoFile = ApplicationPath.MAIN_SEARCH_INFO;
                try {
                    logger.info("Save user search info file to " + ApplicationPath.USER_CONFIG);
                    FileUtils.copyFile(new File(searchInfoFile), usi);

                    logger.debug("Load " + searchInfoFile);
                    FileInputStream fis = new FileInputStream(searchInfoFile);
                    searchProps = ConfigUtils.loadConfig(fis, "UTF-8");
                } catch (Exception e) {
                    logger.error("Error loading search info file " + searchInfoFile);
                    logger.implicitLog(e);
                }
            } else {
                String searchInfoFile = ApplicationPath.USER_SEARCH_INFO;
                try {
                    String ver = null;
                    boolean error = false;
                    try {
                        FileInputStream fis = new FileInputStream(searchInfoFile);
                        searchProps = ConfigUtils.loadConfig(fis, "UTF-8", ApplicationPath.CONFIG_DIR);
                        ver = searchProps.getString("search.version");
                    } catch (Exception e) {
                        logger.error(String.format(
                                "Error loading user search info file %s."
                                        + " Will replace it with original search info file %s.",
                                searchInfoFile, ApplicationPath.MAIN_SEARCH_INFO), e);
                        error = true;
                    }
                    if (!GlobalConfig.ZEKR_VERSION.equals(ver) || error) {
                        searchInfoFile = ApplicationPath.USER_CONFIG;
                        searchProps = ConfigUtils.loadConfig(new FileInputStream(searchInfoFile), "UTF-8");
                        String newName = String.format("%s_%s", res.getString("config.searchInfo.file"),
                                String.format(ver == null ? "old" : ver));
                        logger.info(String.format(
                                "Migrate search info from version %s to %s. Will rename old file to %s.", ver,
                                GlobalConfig.ZEKR_VERSION, newName));
                        FileUtils.copyFile(usi, new File(usi.getParent(), newName));
                        FileUtils.copyFile(new File(ApplicationPath.MAIN_SEARCH_INFO), usi);
                    }
                } catch (Exception e) {
                    logger.error("Error loading search info file " + searchInfoFile);
                    logger.implicitLog(e);
                }
            }

            searchInfo = new SearchInfo();
            Configuration stopWordConf = searchProps.subset("search.stopword");
            List<String> defaultStopWord = searchProps.getList("search.stopword");
            Configuration replacePatternConf = searchProps.subset("search.pattern.replace");
            List<String> defaultReplacePattern = searchProps.getList("search.pattern.replace");
            Configuration punctuationConf = searchProps.subset("search.pattern.punct");
            String defaultPunctuation = searchProps.getString("search.pattern.punct");
            Configuration diacriticsConf = searchProps.subset("search.pattern.diacr");
            String defaultDiacritics = searchProps.getString("search.pattern.diacr");
            Configuration letterConf = searchProps.subset("search.pattern.letter");

            searchInfo.setDefaultStopWord(defaultStopWord);
            for (Iterator<String> iterator = stopWordConf.getKeys(); iterator.hasNext();) {
                String langCode = iterator.next();
                if (langCode.length() <= 0) {
                    continue;
                }
                logger.debug("\tAdd stop words for: " + langCode);
                searchInfo.addStopWord(langCode, stopWordConf.getList(langCode));
            }

            searchInfo.setDefaultReplacePattern(defaultReplacePattern);
            for (Iterator<String> iterator = replacePatternConf.getKeys(); iterator.hasNext();) {
                String langCode = iterator.next();
                if (langCode.length() <= 0) {
                    continue;
                }
                logger.debug("\tAdd replace patterns for: " + langCode);
                searchInfo.addReplacePattern(langCode, replacePatternConf.getList(langCode));
            }

            if (defaultPunctuation != null) {
                searchInfo.setDefaultPunctuation(Pattern.compile(defaultPunctuation));
            }
            for (Iterator<String> iterator = punctuationConf.getKeys(); iterator.hasNext();) {
                String langCode = iterator.next();
                if (langCode.length() <= 0) {
                    continue;
                }
                logger.debug("\tAdd punctuation pattern for: " + langCode);
                searchInfo.setPunctuation(langCode, Pattern.compile(punctuationConf.getString(langCode)));
            }

            if (defaultDiacritics != null) {
                searchInfo.setDefaultDiacritic(Pattern.compile(defaultDiacritics));
            }
            for (Iterator<String> iterator = diacriticsConf.getKeys(); iterator.hasNext();) {
                String langCode = iterator.next();
                if (langCode.length() <= 0) {
                    continue;
                }
                logger.debug("\tAdd diacritics pattern for: " + langCode);
                searchInfo.setDiacritic(langCode, Pattern.compile(diacriticsConf.getString(langCode)));
            }

            for (Iterator<String> iterator = letterConf.getKeys(); iterator.hasNext();) {
                String langCode = iterator.next();
                if (langCode.length() <= 0) {
                    continue;
                }
                logger.debug("\tAdd letters range pattern for: " + langCode);
                searchInfo.setLetter(langCode, Pattern.compile(letterConf.getString(langCode)));
            }
        } catch (Exception ex) {
            logger.error("Search info not initialized correctly because of the next error."
                    + " Zekr, however, will be launched.");
            logger.implicitLog(ex);
        }
    }

    private void loadRootList() {
        try {
            logger.info("Loading Quran root word database...");
            String rootFile = res.getString("text.quran.root");
            String rootRawStr = FileUtils.readFileToString(new File(rootFile), "UTF-8");
            Date date1 = new Date();
            quranRoot = new QuranRoot(rootRawStr);
            Date date2 = new Date();
            logger.debug("Took " + (date2.getTime() - date1.getTime()) + " ms.");
        } catch (IOException ioe) {
            logger.log(ioe);
        }
    }

    private void initViewController() {
        logger.debug("Initialize view controller.");
        userViewController = new UserViewController(quranPaging);
        userViewController.setLocation(getQuranLocation());
        userViewController.synchPage();
    }

    private void initNetworkController() {
        logger.debug("Initialize network controller.");
        networkController = new NetworkController(props);
    }

    /*
    private void startHttpServer() {
       logger.info("Start HTTP server daemon on port: " + getHttpServerPort());
       httpServer = HttpServerFactory.createHttpServer(props);
       if (isHttpServerEnabled()) {
     httpServer.run();
       }
    }
    */

    public static ApplicationConfig getInstance() {
        if (thisInstance == null) {
            thisInstance = new ApplicationConfig();
        }
        return thisInstance;
    }

    @SuppressWarnings("unchecked")
    private void loadConfig() {
        logger.info("Load Zekr configuration file.");
        File uc = new File(ApplicationPath.USER_CONFIG);
        boolean createConfig = false;
        String confFile = ApplicationPath.USER_CONFIG;
        if (!uc.exists()) {
            logger.info("User config does not exist at " + ApplicationPath.USER_CONFIG);
            logger.info("Will make user config with default values at " + ApplicationPath.MAIN_CONFIG);
            confFile = ApplicationPath.MAIN_CONFIG;
            createConfig = true;
        }

        try {
            logger.debug("Load " + confFile);
            props = ConfigUtils.loadConfig(new File(confFile), ApplicationPath.CONFIG_DIR, "UTF-8");

            String version = props.getString("version");
            if (!GlobalConfig.ZEKR_VERSION.equals(version)) {
                logger.info("User config version (" + version + ") does not match " + GlobalConfig.ZEKR_VERSION);

                if (StringUtils.isBlank(version) || !isCompatibleVersion(version)) { // config file is too old
                    logger.info(String.format("Previous version (%s) is too old and not compatible with %s",
                            version, GlobalConfig.ZEKR_VERSION));
                    logger.info("Cannot migrate old settings. Will reset settings.");

                    props = ConfigUtils.loadConfig(new File(ApplicationPath.MAIN_CONFIG), "UTF-8");
                } else {
                    logger.info("Will initialize user config with default values, overriding with old config.");

                    PropertiesConfiguration oldProps = props;
                    props = ConfigUtils.loadConfig(new File(ApplicationPath.MAIN_CONFIG), "UTF-8");

                    for (Iterator<String> iter = oldProps.getKeys(); iter.hasNext();) {
                        String key = iter.next();
                        if (key.equals("version")) {
                            continue;
                        }
                        props.setProperty(key, oldProps.getProperty(key));
                    }
                }
                createConfig = true;
            }
        } catch (Exception e) {
            logger.warn("IO Error in loading/reading config file " + ApplicationPath.MAIN_CONFIG);
            logger.log(e);
        }
        if (createConfig) {
            runtime.clearAll();
            // create config dir
            new File(Naming.getConfigDir()).mkdirs();
            saveConfig();
        }

        // load shortcuts
        logger.info("Loading keyboard shortcuts.");
        File userShortcut = new File(ApplicationPath.USER_SHORTCUT);
        Document doc = null;
        if (userShortcut.exists()) {
            try {
                logger.info("Loading user keyboard shortcuts: " + ApplicationPath.USER_SHORTCUT);
                Document userDoc = new XmlReader(userShortcut).getDocument();
                String version = userDoc.getDocumentElement().getAttribute("version");
                if (GlobalConfig.ZEKR_VERSION.equals(version)) {
                    doc = userDoc;
                } else {
                    logger.info("User shortcut file version (" + version + ") does not match with "
                            + GlobalConfig.ZEKR_VERSION);

                    List<String> userList = new ArrayList<String>();
                    Element userRoot = userDoc.getDocumentElement();
                    NodeList userMappings = userRoot.getElementsByTagName("mapping");
                    for (int i = 0; i < userMappings.getLength(); i++) {
                        Element mapping = (Element) userMappings.item(i);
                        String action = mapping.getAttribute("action");
                        userList.add(action);
                    }

                    File mainShortcut = new File(ApplicationPath.MAIN_SHORTCUT);
                    Element mainRoot = new XmlReader(mainShortcut).getDocument().getDocumentElement();
                    NodeList mainMappings = mainRoot.getElementsByTagName("mapping");
                    for (int i = 0; i < mainMappings.getLength(); i++) {
                        Element mapping = (Element) mainMappings.item(i);
                        String action = mapping.getAttribute("action");
                        if (!userList.contains(action)) {
                            logger.debug("Adding new shortcut mapping for action: " + action);
                            Element newMapping = userDoc.createElement("mapping");
                            newMapping.setAttribute("action", mapping.getAttribute("action"));
                            newMapping.setAttribute("key", mapping.getAttribute("key"));
                            newMapping.setAttribute("rtlKey", mapping.getAttribute("rtlKey"));
                            userRoot.appendChild(newMapping);
                        }
                    }
                    userRoot.setAttribute("version", GlobalConfig.ZEKR_VERSION);
                    doc = userDoc;
                    XmlUtils.writeXml(userDoc, userShortcut);
                }
            } catch (Exception e) {
                logger.warn("Error loading user shortcuts: " + ApplicationPath.USER_SHORTCUT);
                logger.log(e);
            }
        } else {
            try {
                logger.info("Loading keyboard shortcuts from original location: " + ApplicationPath.MAIN_SHORTCUT);
                File mainShortcut = new File(ApplicationPath.MAIN_SHORTCUT);
                doc = new XmlReader(mainShortcut).getDocument();
                FileUtils.copyFile(mainShortcut, new File(ApplicationPath.USER_SHORTCUT));
            } catch (Exception e) {
                logger.log(e);
            }
        }
        if (doc != null) {
            logger.info("Initialize keyboard shortcuts and mappings.");
            shortcut = new KeyboardShortcut(props, doc);
            shortcut.init();
        }
    }

    /**
     * A threshold version is checked here. If user config version is newer or equal to this version, then config file can be
     * migrated. Otherwise, it's reset.
     * 
     * @param version
     * @return
     */
    private boolean isCompatibleVersion(String version) {
        try {
            Pattern regex = Pattern.compile("(\\d+\\.\\d+\\.\\d+).*"); // e.g. 0.7.6 or 0.7.5beta2
            Matcher m = regex.matcher(version);
            if (m.find()) {
                String versionPart = m.group(1);
                return CommonUtils.compareVersions(versionPart, "0.7.5") >= 0;
            }
        } catch (Exception e) {
            logger.implicitLog(e);
            return false;
        }
        return false;
    }

    private void loadBookmarkSetGroup() {
        File bookmarkDir = new File(Naming.getBookmarkDir());
        File origBookmarkDir = new File(res.getString("bookmark.baseDir"));

        FileFilter xmlFilter = new FileFilter() { // accept .xml files
            public boolean accept(File pathname) {
                if (pathname.getName().toLowerCase().endsWith(".xml")) {
                    return true;
                }
                return false;
            }
        };

        // bookmarks
        try {
            if (!bookmarkDir.exists() || !bookmarkDir.isDirectory()) {
                logger.info("Copy all bookmarks to " + Naming.getBookmarkDir());
                FileUtils.copyDirectory(origBookmarkDir, bookmarkDir);
            } else {
                File bookmarkFolderAlreadyCopied = new File(Naming.getBookmarkDir() + "/.DONOTDELETE");
                if (!bookmarkFolderAlreadyCopied.exists()) {
                    File[] origs = origBookmarkDir.listFiles(xmlFilter);
                    for (int i = 0; i < origs.length; i++) {
                        File destFile = new File(bookmarkDir + "/" + origs[i].getName());
                        if (!destFile.exists()) {
                            logger.info("Copy bookmark " + origs[i] + " to " + Naming.getBookmarkDir());
                            FileUtils.copyFile(origs[i], destFile);
                        }
                    }
                }
            }
        } catch (IOException e) {
            logger.log(e);
        }

        String def = props.getString("bookmark.default");
        File[] bookmarkSets = bookmarkDir.listFiles(xmlFilter);
        for (int i = 0; i < bookmarkSets.length; i++) {
            // bookmarks should be lazily loaded
            BookmarkSet bms = new BookmarkSet(Naming.getBookmarkDir() + "/" + bookmarkSets[i].getName());
            bookmarkSetGroup.addBookmarkSet(bms);
            if (bms.getId().equals(def)) {
                bookmarkSetGroup.setAsDefault(bms);
            }
        }
        if (bookmarkSetGroup.getDefault() == null) {
            logger.doFatal(new BookmarkException(
                    "No default bookmark set, or cannot load the default bookmark set: " + def));
        }
        bookmarkSetGroup.getDefault().load();
    }

    /**
     * Save properties configuration file, which was read into <code>props</code>, to {@link ApplicationPath#USER_CONFIG}.
     */
    public void saveConfig() {
        try {
            logger.info("Save user config file to " + ApplicationPath.USER_CONFIG);
            props.save(new FileOutputStream(ApplicationPath.USER_CONFIG), "UTF-8");
        } catch (Exception e) {
            logger.error("Error while saving config to " + ApplicationPath.USER_CONFIG + ": " + e);
        }
    }

    /**
     * @return User configuration properties
     */
    public PropertiesConfiguration getProps() {
        return props;
    }

    /**
     * This method extracts language properties from the corresponding node in the config file.
     */
    private void extractLangProps() {
        boolean update = false;

        String def = props.getString("lang.default");
        File langDir = new File(ApplicationPath.LANGUAGE_DIR);
        logger.info("Loading language pack files info");
        logger.info("Default language pack is " + def);
        FileFilter filter = new FileFilter() { // accept .xml files
            public boolean accept(File pathname) {
                if (pathname.getName().toLowerCase().endsWith(".xml")) {
                    return true;
                }
                return false;
            }
        };
        File[] langs = langDir.listFiles(filter);
        LanguagePack lp;

        logger.info("Found these language packs: " + Arrays.asList(langs));

        for (int i = 0; i < langs.length; i++) {
            XmlReader reader = null;
            try {
                reader = new XmlReader(langs[i]);
            } catch (Exception e) {
                if (langs[i].getName().endsWith("english.xml")) {
                    logger.doFatal(e);
                } else {
                    logger.warn("Cannot open language pack " + def + " due to the following error:");
                    logger.log(e);
                    update = true;
                    props.setProperty("lang.default", "en_US");
                    def = "en_US";
                    logger.warn("Default language pack set to: " + def);
                }
            }
            lp = new LanguagePack();
            lp.file = langs[i].getName();
            Element locale = reader.getElement("locale");
            lp.localizedName = locale.getAttribute("localizedName");
            lp.name = locale.getAttribute("name");
            lp.id = locale.getAttribute("id");
            lp.direction = locale.getAttribute("direction");
            lp.author = reader.getDocumentElement().getAttribute("creator");
            if (lp.localizedName == null) {
                lp.localizedName = lp.name;
            }
            language.add(lp);
            if (lp.id.equals(def)) {
                language.setActiveLanguagePack(def);
            }
        }

        if (update) {
            updateFile();
        }
    }

    /**
     * This method extracts translation properties from the corresponding node in the config file.<br>
     * Will first look inside global translations, and then user-specific ones, overwriting global translations with user-defined
     * ones if duplicates found.
     */
    @SuppressWarnings("unchecked")
    private void extractTransProps() {
        String def = props.getString("trans.default");
        logger.info("Default translation is: " + def);

        String[] paths = { ApplicationPath.TRANSLATION_DIR, Naming.getTransDir() };
        for (int pathIndex = 0; pathIndex < paths.length; pathIndex++) {
            File transDir = new File(paths[pathIndex]);
            if (!transDir.exists()) {
                continue;
            }

            logger.info("Loading translation files info from: " + transDir);
            FileFilter filter = new FileFilter() { // accept zip files
                public boolean accept(File pathname) {
                    if (pathname.getName().toLowerCase().endsWith(ApplicationPath.TRANS_PACK_SUFFIX)) {
                        return true;
                    }
                    return false;
                }
            };
            File[] trans = transDir.listFiles(filter);

            TranslationData td;

            for (int transIndex = 0; transIndex < trans.length; transIndex++) {
                ZipFile zipFile = null;
                try {
                    td = loadTranslationData(trans[transIndex]);
                    if (td == null) {
                        continue;
                    }
                    translation.add(td);
                    if (td.id.equals(def)) {
                        try {
                            td.load();
                            logger.info("Default translation is: " + td);
                            translation.setDefault(td);
                        } catch (TranslationException e) {
                            logger.warn("Cannot load default translation: " + e);
                        }
                    }

                } catch (Exception e) {
                    logger.warn("Can not load translation pack \"" + zipFile
                            + "\" properly because of the following exception:");
                    logger.log(e);
                }
            }
        }
        if (translation.getDefault() == null) {
            logger.error(new ZekrBaseException("Could not find default translation: " + def));
            logger.warn("Will use any English or other translations found.");
            for (TranslationData translationData : translation.getAllTranslation()) {
                if (translationData.locale.getLanguage().equalsIgnoreCase("en")) {
                    logger.info("Trying to set default translation to: " + translationData.getId());
                    try {
                        translationData.load();
                        translation.setDefault(translationData);
                        props.setProperty("trans.default", translation.getDefault().id);
                        break;
                    } catch (TranslationException e) {
                        logger.warn("Cannot load default translation: " + e);
                    }
                }
            }
            if (translation.getDefault() == null) {
                logger.warn("No default translation found! Will start without any translation. "
                        + "As a result some features will be disabled.");
                Iterator<TranslationData> iter = translation.getAllTranslation().iterator();
                if (iter.hasNext()) {
                    TranslationData td = iter.next();
                    try {
                        td.load();
                        translation.setDefault(td);
                        props.setProperty("trans.default", translation.getDefault().id);
                        logger.info("Default translation set to: " + translation.getDefault().getId());
                    } catch (TranslationException e) {
                        logger.warn("Cannot load default translation: " + e);
                    }
                }
            }
        }

        if (translation.getDefault() != null) {
            // load custom translation list
            logger.info("Load custom translation list.");
            List<TranslationData> customList = translation.getCustomGroup();
            List<String> customs = props.getList("trans.custom");
            for (int i = 0; i < customs.size(); i++) {
                String tid = customs.get(i);
                if (tid == null || "".equals(tid.trim())) {
                    logger.info("No custom translation list to load.");
                    continue;
                }
                TranslationData td = translation.get(tid);
                if (td == null) {
                    logger.error("No such translation: " + tid);
                    continue;
                }
                try {
                    td.load();
                    customList.add(td);
                } catch (TranslationException e) {
                    logger.warn("Invalid translation will be removed from the multi-translation list: " + e);
                    customs.remove(i);
                }
            }
        } else {
            logger.warn("No translation found!");
        }
    }

    public TranslationData loadTranslationData(File transZipFile) throws IOException, ConfigurationException {
        TranslationData td = null;
        ZipFile zipFile = null;
        try {
            zipFile = new ZipFile(transZipFile);
            InputStream is = zipFile.getInputStream(new ZipEntry(ApplicationPath.TRANSLATION_DESC));
            if (is == null) {
                logger.warn("Will ignore invalid translation archive \"" + zipFile.getName() + "\".");
                return null;
            }
            Reader reader = new InputStreamReader(is, "UTF-8");
            PropertiesConfiguration pc = new PropertiesConfiguration();
            pc.load(reader);
            reader.close();
            is.close();

            td = new TranslationData();
            td.version = pc.getString(VERSION_ATTR);
            td.id = pc.getString(ID_ATTR);
            td.locale = new Locale(pc.getString(LANG_ATTR, "en"), pc.getString(COUNTRY_ATTR, "US"));
            td.encoding = pc.getString(ENCODING_ATTR, "ISO-8859-1");
            td.direction = pc.getString(DIRECTION_ATTR, "ltr");
            td.file = pc.getString(FILE_ATTR);
            td.name = pc.getString(NAME_ATTR);
            td.localizedName = pc.getString(LOCALIZED_NAME_ATTR, td.name);
            td.archiveFile = transZipFile;
            td.delimiter = pc.getString(LINE_DELIMITER_ATTR, "\n");
            String sig = pc.getString(SIGNATURE_ATTR);
            td.signature = sig == null ? null : Base64.decodeBase64(sig.getBytes("US-ASCII"));

            //create a LocalizedInstance for this translation.
            // <patch>
            LocalizedResource localizedResource = new LocalizedResource();
            localizedResource.loadLocalizedNames(pc, NAME_ATTR);
            localizedResource.setLanguage(td.locale.getLanguage());
            td.setLocalizedResource(localizedResource);
            td.setFile(transZipFile);
            // </patch>

            if (StringUtils.isBlank(td.id) || StringUtils.isBlank(td.name) || StringUtils.isBlank(td.file)
                    || StringUtils.isBlank(td.version)) {
                logger.warn("Invalid translation: \"" + td + "\".");
                return null;
            }

            if (zipFile.getEntry(td.file) == null) {
                logger.warn("Invalid translation format. File not exists in the archive: " + td.file);
                return null;
            }
        } finally {
            if (zipFile != null) {
                ZipUtils.closeQuietly(zipFile);
            }
        }

        return td;
    }

    @SuppressWarnings("unchecked")
    private void extractViewProps() {
        ThemeData td;
        Reader reader;
        String def = props.getString("theme.default");
        logger.info("Loading theme .properties files.");

        String[] paths = { ApplicationPath.THEME_DIR, Naming.getThemeDir() };
        for (int pathIndex = 0; pathIndex < paths.length; pathIndex++) {
            File targetThemeDir = new File(paths[pathIndex]);
            if (!targetThemeDir.exists()) {
                continue;
            }

            logger.info("Loading theme files info from \"" + paths[pathIndex]);
            File[] targetThemes = targetThemeDir.listFiles();

            File origThemeDir = new File(paths[pathIndex]);
            File[] origThemes = origThemeDir.listFiles();
            for (int i = 0; i < origThemes.length; i++) {
                String targetThemeDesc = Naming.getThemePropsDir() + "/" + origThemes[i].getName() + ".properties";
                File origThemeDesc = new File(origThemes[i] + "/" + ApplicationPath.THEME_DESC);
                File targetThemeFile = new File(targetThemeDesc);

                if (!origThemeDesc.exists()) {
                    logger.warn("\"" + origThemes[i] + "\" is not a standard theme! Will ignore it.");
                    continue;
                }

                try {
                    if (!targetThemeFile.exists() || FileUtils.isFileNewer(origThemeDesc, targetThemeFile)) {
                        logger.info("Copy theme " + origThemes[i].getName() + " to " + Naming.getThemePropsDir());
                        FileUtils.copyFile(origThemeDesc, targetThemeFile);
                    }
                    FileInputStream fis = new FileInputStream(targetThemeFile);
                    reader = new InputStreamReader(fis, "UTF-8");
                    PropertiesConfiguration pc = new PropertiesConfiguration();
                    pc.load(reader);
                    reader.close();
                    fis.close();

                    td = new ThemeData();
                    td.props = new LinkedHashMap<String, String>(); // order is important for options table!
                    for (Iterator<String> iter = pc.getKeys(); iter.hasNext();) {
                        String key = iter.next();
                        td.props.put(key, CollectionUtils.toString(pc.getList(key), ", "));
                    }
                    td.author = pc.getString("author");
                    td.name = pc.getString("name");
                    td.version = pc.getString("version");
                    td.id = origThemes[i].getName();
                    td.fileName = targetThemeFile.getName();
                    td.baseDir = paths[pathIndex];
                    td.props.remove("author");
                    td.props.remove("name");
                    td.props.remove("version");

                    // extractTransProps must be called before it!
                    if (getTranslation().getDefault() != null) {
                        td.process(getTranslation().getDefault().locale.getLanguage());
                    } else {
                        td.process("en");
                    }

                    theme.add(td);

                    if (td.id.equals(def)) {
                        theme.setCurrent(td);
                    }
                } catch (Exception e) {
                    logger.warn("Can not load theme \"" + targetThemes[i].getName()
                            + "\", because of the following exception:");
                    logger.log(e);
                }
            }
        }
        if (theme.getCurrent() == null) {
            logger.doFatal(new ZekrBaseException("Could not find default theme: " + def));
        }
    }

    @SuppressWarnings("unchecked")
    private void extractAudioProps() {
        String def = props.getString("audio.default");
        List<String> selectedList = props.getList("audio.default");
        if (org.apache.commons.collections.CollectionUtils.isNotEmpty(selectedList) && selectedList.size() > 1) {
            def = selectedList.get(0);
        }
        logger.info("Loading audio .properties files.");

        String[] paths = { ApplicationPath.AUDIO_DIR, Naming.getAudioDir() };
        for (int pathIndex = 0; pathIndex < paths.length; pathIndex++) {
            File audioDir = new File(paths[pathIndex]);
            if (!audioDir.exists()) {
                continue;
            }

            logger.info("Loading audio files info from: " + audioDir);
            FileFilter filter = new FileFilter() { // accept .properties files
                public boolean accept(File pathname) {
                    if (pathname.getName().toLowerCase().endsWith(".properties")) {
                        return true;
                    }
                    return false;
                }
            };
            File[] audioPropFiles = audioDir.listFiles(filter);

            for (int audioIndex = 0; audioIndex < audioPropFiles.length; audioIndex++) {
                try {
                    AudioData audioData = loadAudioData(audioPropFiles[audioIndex], true);
                    if (audioData == null || audioData.getId() == null) {
                        continue;
                    }

                    audio.add(audioData);
                    if (audioData.id.equals(def)) {
                        logger.info("Default recitation is: " + audioData);
                        audio.setCurrent(audioData);
                    }
                } catch (Exception e) {
                    logger.warn("Can not load audio pack \"" + audioPropFiles[audioIndex]
                            + "\" properly because of the following exception:");
                    logger.log(e);
                }
            }
        }

        if (audio.getCurrent() == null) {
            logger.error("No default recitation found: " + def);
            if (audio.getAllAudio().size() > 0) {
                for (AudioData ad : audio.getAllAudio()) {
                    if ("offline".equals(ad.type)) {
                        audio.setCurrent(ad);
                        props.setProperty("audio.default", ad.id);
                        logger.warn("Setting another recitation as default: " + audio.getCurrent());
                        break;
                    }
                }
                if (audio.getCurrent() == null) {
                    audio.setCurrent(audio.getAllAudio().iterator().next());
                    props.setProperty("audio.default", audio.getCurrent().id);
                    logger.warn("Setting another recitation as default: " + audio.getCurrent());
                }
            } else {
                logger.warn("No other recitation found. Audio will be disabled.");
            }
        }

        // load if list of default audio data
        if (audio.getCurrent() != null) {
            // audio.getCurrentList().add(audio.getCurrent());
            for (String audioId : selectedList) {
                AudioData ad = audio.get(audioId);
                if (ad != null) {
                    audio.getCurrentList().add(ad);
                }
            }
            if (audio.getCurrentList().size() <= 0) {
                audio.getCurrentList().add(audio.getCurrent());
            }
        }
    }

    @SuppressWarnings("unchecked")
    public AudioData loadAudioData(File audioFile, boolean convertOldFormat)
            throws FileNotFoundException, UnsupportedEncodingException, ConfigurationException, IOException {
        PropertiesConfiguration pc = ConfigUtils.loadConfig(audioFile, "UTF-8");

        AudioData audioData;
        audioData = new AudioData();
        audioData.id = pc.getString("audio.id");
        audioData.file = audioFile;
        // note that audio.version should be made up of digits and dots only, so 0.7.5beta1 is invalid.
        audioData.version = pc.getString("audio.version");
        if (StringUtils.isBlank(audioData.version)) { // old format
            logger.warn("Not a valid recitation file. No version specified: " + audioFile);
            if (convertOldFormat) {
                logger.info("Will try to convert recitation file: " + audioFile);
                audioData = RecitationPackConverter.convert(audioFile);
                if (audioData == null) {
                    logger.info("Conversion failed for " + audioFile);
                    return null;
                }
                File destDir = new File(
                        FilenameUtils.getFullPath(audioFile.getAbsolutePath()) + "old-recitation-files");
                logger.info(String.format("Move %s to %s.", audioFile, destDir));
                FileUtils.moveFileToDirectory(audioFile, destDir, true);

                Writer w = new FileWriter(audioFile);
                StringWriter sw = new StringWriter();
                audioData.save(sw);
                w.write(sw.toString());
                IOUtils.closeQuietly(w);
                return audioData;
            } else {
                return null;
            }
        } else if (CommonUtils.compareVersions(audioData.version, AudioData.BASE_VALID_VERSION) < 0) {
            logger.warn(String.format(
                    "Version is not supported anymore: %s. Zekr supports a recitation file of version %s or newer.",
                    audioData.version, AudioData.BASE_VALID_VERSION));
            return null;
        }
        audioData.lastUpdate = pc.getString("audio.lastUpdate");
        audioData.quality = pc.getString("audio.quality", "?");

        // audioData.name = pc.getString("audio.name");
        audioData.license = pc.getString("audio.license");
        audioData.locale = new Locale(pc.getString("audio.language"), pc.getString("audio.country"));
        audioData.type = pc.getString("audio.type", "online");

        audioData.setLanguage(audioData.locale.getDisplayLanguage());//this will make it accessible from LocateResource super class.
        audioData.loadLocalizedNames(pc, "audio.reciter");

        Iterator<String> keys = pc.getKeys("audio.reciter");
        while (keys.hasNext()) {
            String key = keys.next();
            if (key.equals("audio.reciter")) {
                continue;
            }
            String lang = key.substring("audio.reciter".length() + 1);
            audioData.localizedNameMap.put(lang, pc.getString(key));
        }

        audioData.offlineUrl = pc.getString("audio.offlineUrl");
        audioData.onlineUrl = pc.getString("audio.onlineUrl");

        audioData.onlineAudhubillah = pc.getString("audio.onlineAudhubillah");
        // keep backward compatibility for old typo in files (bismillam instead of bismillah)
        audioData.onlineBismillah = pc.getString("audio.onlineBismillah", pc.getString("audio.onlineBismillam"));
        // keep backward compatibility for old typo in files (saghaghallah instead of sadaghallah)
        audioData.onlineSadaghallah = pc.getString("audio.onlineSadaghallah",
                pc.getString("audio.onlineSaghaghallah"));

        audioData.offlineAudhubillah = pc.getString("audio.offlineAudhubillah");
        // keep backward compatibility for old typo in files (bismillam instead of bismillah)
        audioData.offlineBismillah = pc.getString("audio.offlineBismillah", pc.getString("audio.offlineBismillam"));
        // keep backward compatibility for old typo in files (saghaghallah instead of sadaghallah)
        audioData.offlineSadaghallah = pc.getString("audio.offlineSadaghallah",
                pc.getString("audio.offlineSaghaghallah"));
        return audioData;
    }

    private void setupAudioManager() {
        audioCacheManager = new AudioCacheManager(props);
        // long period = props.getLong("audio.cache.timerPeriod", 3600000);
        // start after one minute, run every audio.cache.timerPeriod milliseconds
        // logger.debug("Setup audio cache timer task.");
        // new Timer("Audio Cache Task", true).schedule(new AudioCacheManagerTimerTask(audioCacheManager), 60000, period);

        logger.debug("Initialize player controller.");
        playerController = new DefaultPlayerController(props);
        searchPlayerController = new DefaultPlayerController(props);
    }

    private void extractRevelOrderInfo() {
        String def = props.getString("revel.default");
        logger.info("Default revelation package is: " + def);

        File revelDir = new File(ApplicationPath.REVELATION_DIR);
        if (!revelDir.exists()) {
            logger.debug("No revelation data pack found.");
            return;
        }

        logger.info("Loading revelation data packs from: " + revelDir);
        FileFilter filter = new FileFilter() { // accept zip files
            public boolean accept(File pathname) {
                if (pathname.getName().toLowerCase().endsWith(ApplicationPath.REVEL_PACK_SUFFIX)) {
                    return true;
                }
                return false;
            }
        };
        File[] revelFiles = revelDir.listFiles(filter);

        RevelationData rd;
        for (int revelIndex = 0; revelIndex < revelFiles.length; revelIndex++) {
            ZipFile zipFile = null;
            try {
                rd = loadRevelationData(revelFiles[revelIndex]);
                if (rd == null) {
                    continue;
                }
                revelation.add(rd);
                if (rd.id.equals(def)) {
                    rd.load();
                    logger.info("Default revelation data is: " + rd);
                    revelation.setDefault(rd);
                }
            } catch (Exception e) {
                logger.warn("Can not load revelation data pack \"" + zipFile
                        + "\" properly because of the following exception:");
                logger.log(e);
            }
        }
    }

    private RevelationData loadRevelationData(File revelZipFile) throws IOException, ConfigurationException {
        ZipFile zipFile = new ZipFile(revelZipFile);
        InputStream is = zipFile.getInputStream(new ZipEntry(ApplicationPath.REVELATION_DESC));
        if (is == null) {
            logger.warn("Will ignore invalid revelation data archive \"" + zipFile.getName() + "\".");
            return null;
        }
        PropertiesConfiguration pc = ConfigUtils.loadConfig(is, "UTF-8");
        zipFile.close();

        RevelationData rd = new RevelationData();

        int len;
        if ("aya".equals(pc.getString("mode", "sura"))) {
            len = QuranPropertiesUtils.QURAN_AYA_COUNT;
            rd.mode = RevelationData.AYA_MODE;
        } else {
            len = 114;
            rd.mode = RevelationData.SURA_MODE;
        }
        rd.suraOrders = new int[len];
        rd.orders = new int[len];
        // rd.years = new int[len]; // not used for now

        rd.version = pc.getString("version");
        String zipFileName = revelZipFile.getName();
        rd.id = zipFileName.substring(0, zipFileName.length() - ApplicationPath.REVEL_PACK_SUFFIX.length());
        rd.archiveFile = revelZipFile;
        rd.delimiter = pc.getString("delimiter", "\n");
        String sig = pc.getString("signature");

        byte[] sigBytes = sig.getBytes("US-ASCII");
        rd.signature = sig == null ? null : Base64.decodeBase64(sigBytes);

        rd.loadLocalizedNames(pc, "name");

        if (StringUtils.isBlank(rd.id) || rd.localizedNameMap.size() == 0 || StringUtils.isBlank(rd.version)) {
            logger.warn("Invalid revelation data package: \"" + rd + "\".");
            return null;
        }
        return rd;
    }

    private void extractPagingDataProps() {
        String def = props.getString("view.pagingMode");
        logger.info("Default paging mode is: " + def);

        File pagingDir = new File(ApplicationPath.PAGING_DIR);
        if (!pagingDir.exists()) {
            logger.debug("No paging data found.");
            return;
        }

        logger.info("Loading paging data from: " + pagingDir);
        FileFilter filter = new FileFilter() {
            public boolean accept(File pathname) {
                if (pathname.getName().toLowerCase().endsWith(ApplicationPath.PAGING_PACK_SUFFIX)) {
                    return true;
                }
                return false;
            }
        };
        File[] pagingFiles = pagingDir.listFiles(filter);

        // add built-in paging implementations
        quranPaging.add(new SuraPagingData());
        quranPaging.add(new FixedAyaPagingData(props.getInt("view.pagingMode.ayaPerPage", 20)));
        quranPaging.add(new HizbQuarterPagingData());
        quranPaging.add(new JuzPagingData());

        CustomPagingData cpd;
        for (int i = 0; i < pagingFiles.length; i++) {
            cpd = new CustomPagingData();
            String name = pagingFiles[i].getName();
            cpd.setId(name.substring(0, name.indexOf(ApplicationPath.PAGING_PACK_SUFFIX)));
            cpd.file = pagingFiles[i];
            quranPaging.add(cpd);
        }
        IPagingData ipd = (IPagingData) quranPaging.get(def);
        if (ipd != null) {
            try {
                logger.info("Default paging data is: " + ipd);
                ipd.load();
                logger.info("Default paging data loaded successfully: " + ipd);
                quranPaging.setDefault(ipd);
            } catch (Exception e) {
                logger.warn(
                        "Can not load paging data \"" + ipd + "\" properly because of the following exception:");
                logger.log(e);
                logger.debug("Set default paging data to: sura.");
                // set default paging model to sura, if nothing is set.
                quranPaging.setDefault(quranPaging.get(SuraPagingData.ID));
                props.setProperty("view.pagingMode", quranPaging.getDefault().getId());
            }
        }
        if (quranPaging.getDefault() == null) {
            logger.warn("No default paging data found. Will load Hizb Quarter paging data.");
            quranPaging.setDefault(quranPaging.get(HizbQuarterPagingData.ID));
        }
    }

    /**
     * @return application language engine
     * @see Language#getInstance()
     */
    public synchronized LanguageEngine getLanguageEngine() {
        if (langEngine == null) {
            langEngine = LanguageEngine.getInstance();
        }
        return langEngine;
    }

    public void setCurrentLanguage(String langId) {
        logger.info("Set current language to " + langId);
        language.setActiveLanguagePack(langId);
        langEngine.reload();
        logger.debug("Update localized sura names if available.");
        QuranPropertiesUtils.updateLocalizedSuraNames();
        props.setProperty("lang.default", langId);
    }

    public void setCurrentTheme(String themeId) {
        logger.info("Set current theme to " + themeId);
        theme.setCurrent(theme.get(themeId));
        props.setProperty("theme.default", themeId);
    }

    public void setCurrentTranslation(String transId) throws TranslationException {
        boolean unloadPrevTrans = true;
        String defId = translation.getDefault().id;

        if (defId.equals(transId)) {
            logger.info("Translation is already selected: " + transId);
        }

        logger.info("Change default translation: " + defId + " => " + transId);

        for (Iterator<TranslationData> iterator = translation.getCustomGroup().iterator(); iterator.hasNext();) {
            TranslationData td = iterator.next();
            if (td.id.equals(defId)) {
                unloadPrevTrans = false;
                break;
            }
        }

        TranslationData oldTd = translation.getDefault();
        TranslationData newTrans = getTranslation().get(transId);
        newTrans.load();
        translation.setDefault(newTrans);
        props.setProperty("trans.default", transId);

        if (unloadPrevTrans) {
            logger.info("Unload previous selected translation which is not used anymore: " + oldTd);
            oldTd.unloadTranslationDataFile();
        }

        try {
            runtime.recreateViewCache();
        } catch (IOException e) {
            logger.log(e);
        }
    }

    /**
     * @param audioId pass null to remove this audio
     * @param reciterIndex
     */
    public void setSelectedAudio(String audioId, int reciterIndex) {
        AudioData ad;
        if (audioId != null) { // add
            logger.info(String.format("Set selected recitation to: %s, index: %s", audioId, reciterIndex));
            ad = audio.get(audioId);

            if (reciterIndex == 0) {
                audio.setCurrent(ad);
            }

            // ensure size
            while (audio.getCurrentList().size() < reciterIndex + 1) {
                /*if (audio.getCurrentList().size() < reciterIndex) {*/
                audio.getCurrentList().add(null);
                /*}*/
            }
            audio.getCurrentList().set(reciterIndex, ad);
        } else { // remove
            assert reciterIndex < audio.getCurrentList()
                    .size() : "reciter index to remove is larger than selected recitation list size";
            if (reciterIndex <= 0) {
                throw new IllegalArgumentException("First recitation cannot be deleted");
            }
            ad = audio.getCurrentList().get(reciterIndex);
            logger.info(String.format("Remove selected recitation from index: %s, id: %s", reciterIndex,
                    audio.getCurrentList().get(reciterIndex).id));
            audio.getCurrentList().remove(reciterIndex);
        }

        // props.setProperty("audio.default", audioId);
        props.setProperty("audio.default", audio.getCurrentIdList());

        try {
            // runtime.recreateViewCache(); // this is probably historical and is no more needed
            // runtime.recreatePlaylistCache(); // not really needed
        } catch (Exception e) {
            logger.log(e);
        }
    }

    public String getViewProp(String propKey) {
        return props.getString(propKey);
    }

    public void setViewProp(String propKey, String value) {
        props.setProperty(propKey, value);
    }

    public String getQuranLayout() {
        return props.getString("view.quranLayout");
    }

    public void setQuranLayout(String newLayout) {
        props.setProperty("view.quranLayout", newLayout);
    }

    public int getPageNum() {
        return props.getInt("view.page", 1);
    }

    public IQuranLocation getQuranLocation() {
        return new QuranLocation(props.getString("view.quranLoc"));
    }

    public void setQuranLocation(IQuranLocation quranLocation) {
        props.setProperty("view.quranLoc", quranLocation);
    }

    public String getTransLayout() {
        return props.getString("view.transLayout");
    }

    public void setTransLayout(String newLayout) {
        props.setProperty("view.transLayout", newLayout);
    }

    public void setViewLayout(String layout) {
        props.setProperty("view.viewLayout", layout);
    }

    public String getViewLayout() {
        return props.getString("view.viewLayout");
    }

    public void setPagingMode(String pagingModeId) {
        try {
            IPagingData pagingData = getQuranPaging().get(pagingModeId);
            if (pagingData == null) {
                logger.warn("No such paging data: " + pagingModeId);
                return;
            }
            logger.info("Change current paging mode to to " + pagingModeId);
            pagingData.load(); // ensure that paging data is loaded
            quranPaging.setDefault(pagingData);
            props.setProperty("view.pagingMode", pagingModeId);

            runtime.recreateViewCache(); // HTML files are not valid anymore from paging POV
            runtime.recreatePlaylistCache(); // playlists are not valid anymore from paging POV
        } catch (Exception e) {
            logger.log(e);
        }
    }

    public String getPagingMode() {
        return props.getString("view.pagingMode");
    }

    public boolean isHttpServerEnabled() {
        // return props.getBoolean("server.http.enable");
        return false;
    }

    public boolean isRootDatabaseEnabled() {
        return props.getBoolean("root.enable", true);
    }

    public boolean useMozilla() {
        // TODO: remove this property and use something like options.browser.mode = mozilla, webkit, etc.
        return props.getBoolean("options.browser.useMozilla");
    }

    /**
     * @return HTTP server port or -1 if nothing found.
     */
    public int getHttpServerPort() {
        String port = props.getString("server.http.port");
        return port == null ? -1 : Integer.parseInt(port);
    }

    public Language getLanguage() {
        return language;
    }

    public void updateFile() {
        logger.info("Update configuration file.");
        saveConfig();
    }

    public Translation getTranslation() {
        return translation;
    }

    public Theme getTheme() {
        return theme;
    }

    public Audio getAudio() {
        return audio;
    }

    public Revelation getRevelation() {
        return revelation;
    }

    public QuranPaging getQuranPaging() {
        return quranPaging;
    }

    public QuranRoot getQuranRoot() {
        return quranRoot;
    }

    public SearchInfo getSearchInfo() {
        return searchInfo;
    }

    public HttpServer getHttpServer() {
        // return httpServer;
        return null;
    }

    public ApplicationRuntime getRuntime() {
        return runtime;
    }

    public void setRuntime(ApplicationRuntime runtime) {
        this.runtime = runtime;
    }

    public BookmarkSetGroup getBookmarkSetGroup() {
        return bookmarkSetGroup;
    }

    public BookmarkSet getBookmark() {
        return bookmarkSetGroup.getDefault();
    }

    public IUserView getUserViewController() {
        return userViewController;
    }

    /**
     * @return <code>true</code> if an instance of this class is initialized, and <code>false</code> otherwise.
     */
    public static boolean isFullyInitialized() {
        return thisInstance != null;
    }

    public void setShowSplash(boolean showSplash) {
        File splashFile = new File(Naming.getConfigDir() + "/.DONTSHOWSPASH");
        if (showSplash) {
            splashFile.delete();
        } else {
            try {
                splashFile.createNewFile();
            } catch (IOException e) {
                logger.error("Error changing show splash property: " + e.getMessage());
            }
        }
    }

    public boolean getShowSplash() {
        File splashFile = new File(Naming.getConfigDir() + "/.DONTSHOWSPASH");
        return !splashFile.exists();
    }

    /**
     * @return A list of <code>TranslationData</code>
     */
    public List<TranslationData> getCustomTranslationList() {
        return translation.getCustomGroup();
    }

    /**
     * @param newIdList a list of new translation data IDs (list contains Strings).
     * @throws TranslationException
     */
    public void setCustomTranslationList(List<String> newIdList) throws TranslationException {
        List<TranslationData> newList = new ArrayList<TranslationData>();

        // load new translation packs
        for (int i = 0; i < newIdList.size(); i++) {
            String id = newIdList.get(i);
            TranslationData td = translation.get(id);
            td.load();
            newList.add(td);
        }

        String defaultId = translation.getDefault().id;

        // unload old translation packs (which are not included in the new list)
        List<TranslationData> oldCustomList = translation.getCustomGroup();
        for (int i = 0; i < oldCustomList.size(); i++) {
            TranslationData oldTd = oldCustomList.get(i);
            if (!newIdList.contains(oldTd.id) && !oldTd.id.equals(defaultId)) {
                logger.info("Unload previous selected translation which is not used anymore: " + oldTd);
                oldTd.unloadTranslationDataFile();
            }
        }

        translation.setCustomGroup(newList);

        props.setProperty("trans.custom", newIdList);
        saveConfig();
    }

    public LuceneIndexManager getLuceneIndexManager() {
        return luceneIndexManager;
    }

    public boolean isAudioEnabled() {
        return props.getBoolean("audio.enable");
    }

    /**
     * This method is used to add a new translation during runtime. It loads translation metadata and adds it to the list of
     * translations. If translation pack is not authentic, it throws a ZekrMessageException just to inform user.
     * 
     * @param transFile a translation zip archive to be loaded
     * @throws ZekrMessageException with the proper message key and parameters if any exception occurred
     */
    public TranslationData addNewTranslation(File transFile) throws ZekrMessageException {
        logger.debug("Add new translation: " + transFile);
        try {
            TranslationData td = loadTranslationData(transFile);
            if (td == null) {
                throw new ZekrMessageException("INVALID_TRANSLATION_FORMAT", new String[] { transFile.getName() });
            }
            translation.add(td);
            if (!td.verify())
                throw new InvalidResourceException("Translation failed to verify");
            else
                return td;
        } catch (ZekrMessageException zme) {
            throw zme;
        } catch (Exception e) {
            throw new ZekrMessageException("TRANSLATION_LOAD_FAILED",
                    new String[] { transFile.getName(), e.toString() });
        }

    }

    public AudioData addNewRecitationPack(File zipFileToImport, String destDir,
            IntallationProgressListener progressListener) throws ZekrMessageException {
        try {
            ZipFile zipFile = new ZipFile(zipFileToImport);
            InputStream is = zipFile.getInputStream(new ZipEntry(ApplicationPath.RECITATION_DESC));
            if (is == null) {
                logger.debug(
                        String.format("Could not find recitation descriptor %s in the root of the zip archive %s.",
                                zipFileToImport, ApplicationPath.RECITATION_DESC));
                throw new ZekrMessageException("INVALID_RECITATION_FORMAT",
                        new String[] { zipFileToImport.getName() });
            }

            String tempFileName = System.currentTimeMillis() + "-" + ApplicationPath.RECITATION_DESC;
            tempFileName = System.getProperty("java.io.tmpdir") + "/" + tempFileName;
            File recitPropsFile = new File(tempFileName);
            OutputStreamWriter output = null;
            InputStreamReader input = null;
            try {
                output = new OutputStreamWriter(new FileOutputStream(recitPropsFile), "UTF-8");
                input = new InputStreamReader(is, "UTF-8");
                IOUtils.copy(input, output);
            } finally {
                IOUtils.closeQuietly(output);
                IOUtils.closeQuietly(input);
            }
            logger.debug("Add new recitation: " + recitPropsFile);

            AudioData newAudioData = loadAudioData(recitPropsFile, false);
            if (newAudioData == null || newAudioData.getId() == null) {
                logger.debug("Invalid recitation descriptor: " + recitPropsFile);
                throw new ZekrMessageException("INVALID_RECITATION_FORMAT",
                        new String[] { zipFileToImport.getName() });
            }
            File newRecitPropsFile = new File(destDir, newAudioData.id + ".properties");

            if (newRecitPropsFile.exists()) {
                newRecitPropsFile.delete();
            }
            FileUtils.moveFile(recitPropsFile, newRecitPropsFile);

            /*
            ZipEntry recFolderEntry = zipFile.getEntry(newAudioData.id);
            if (recFolderEntry == null || !recFolderEntry.isDirectory()) {
               logger.warn(String.format("Recitation audio folder (%s) doesn't exist in the root of archive %s.",
              newAudioData.id, zipFileToImport));
               throw new ZekrMessageException("INVALID_RECITATION_FORMAT", new String[] { zipFileToImport.getName() });
            }
            */

            AudioData installedAudioData = audio.get(newAudioData.id);
            if (installedAudioData != null) {
                if (newAudioData.compareTo(installedAudioData) < 0) {
                    throw new ZekrMessageException("NEWER_VERSION_INSTALLED", new String[] {
                            recitPropsFile.toString(), newAudioData.lastUpdate, installedAudioData.lastUpdate });
                }
            }

            newAudioData.file = newRecitPropsFile;

            logger.info(String.format("Start uncompressing recitation: %s with size: %s to %s.",
                    zipFileToImport.getName(), FileUtils.byteCountToDisplaySize(zipFileToImport.length()),
                    destDir));

            boolean result;
            try {
                result = ZipUtils.extract(zipFileToImport, destDir, progressListener);
            } finally {
                File file = new File(newRecitPropsFile.getParent(), ApplicationPath.RECITATION_DESC);
                if (file.exists()) {
                    FileUtils.deleteQuietly(file);
                }
            }
            if (result) {
                logger.info("Uncompressing process done: " + zipFileToImport.getName());
                audio.add(newAudioData);
            } else {
                logger.info("Uncompressing process intrrrupted: " + zipFileToImport.getName());
            }

            // FileUtils.deleteQuietly(new File(newRecitPropsFile.getParent(), ApplicationPath.RECITATION_DESC));

            progressListener.finish(newAudioData);

            return result ? newAudioData : null;
        } catch (ZekrMessageException zme) {
            throw zme;
        } catch (Exception e) {
            logger.error("Error occurred while adding new recitation archive.", e);
            throw new ZekrMessageException("RECITATION_LOAD_FAILED",
                    new String[] { zipFileToImport.getName(), e.toString() });
        }
    }

    public AudioData addNewRecitation(File recitFile) throws ZekrMessageException {
        logger.debug("Add new recitation: " + recitFile);
        try {
            AudioData newAudioData = loadAudioData(recitFile, true);
            if (newAudioData == null || newAudioData.getId() == null) {
                throw new ZekrMessageException("INVALID_RECITATION_FORMAT", new String[] { recitFile.getName() });
            }
            AudioData installedAudioData = audio.get(newAudioData.id);
            if (installedAudioData != null) {
                if (newAudioData.compareTo(installedAudioData) < 0) {
                    throw new ZekrMessageException("NEWER_VERSION_INSTALLED", new String[] { recitFile.toString(),
                            newAudioData.lastUpdate, installedAudioData.lastUpdate });
                }
            }
            audio.add(newAudioData);
            return newAudioData;
        } catch (ZekrMessageException zme) {
            throw zme;
        } catch (Exception e) {
            throw new ZekrMessageException("RECITATION_LOAD_FAILED",
                    new String[] { recitFile.getName(), e.toString() });
        }
    }

    public AudioCacheManager getAudioCacheManager() {
        return audioCacheManager;
    }

    public PlayerController getPlayerController() {
        return playerController;
    }

    public PlayerController getSearchPlayerController() {
        return searchPlayerController;
    }

    public NetworkController getNetworkController() {
        return networkController;
    }

    public KeyboardShortcut getShortcut() {
        return shortcut;
    }

    // <patch>
    /**
     * @param r
     * @return
     */
    /*@SuppressWarnings("unchecked")
    synchronized public boolean isCurrentlyInstalled(Resource r) {
       String configurationKey = "resources." + r.getType().getSimpleName();
       List idList = props.getList(configurationKey);
       if (idList.contains(r.getId()))
     return true;
       else
     return false;
    }
    */
    synchronized public Resource installResource(CandidateResource r, IntallationProgressListener progressListener)
            throws ZekrMessageException {
        File newInstalledFile = null;
        try {
            newInstalledFile = new File(r.getInstallationFolder() + "/" + r.getFile().getName());
            FileUtils.copyFile(r.getFile(), newInstalledFile);
            if (r.getType().equals(TranslationData.class)) {
                r.setInstalledResource(addNewTranslation(newInstalledFile));
            } else if (r.getType().equals(AudioData.class)) {
                if (r.getFile().getName().contains("offline"))//a little bit of a hack ;)
                    r.setInstalledResource(
                            addNewRecitationPack(newInstalledFile, ApplicationPath.AUDIO_DIR, progressListener));
                else if (r.getFile().getName().contains("online"))
                    r.setInstalledResource(addNewRecitation(newInstalledFile));
            } else
                throw new InvalidParameterException("ResourceType not been implement yet");

            /*String configurationKey = "resources." + r.getType().getSimpleName();
            List idList = props.getList(configurationKey);
            if (!idList.contains(r.getInstalledResource().getId()))
               idList.add(r.getInstalledResource().getId());
            props.setProperty(configurationKey, idList);
            saveConfig();*/
            return r.getInstalledResource();
        } catch (IOException e) {
            if (newInstalledFile != null)
                FileUtils.deleteQuietly(newInstalledFile);
            e.printStackTrace();
            throw new ZekrMessageException(e);
        }
        /*} catch (ConfigurationException e) {
           e.printStackTrace();
           throw new ZekrMessageException(e);
        }*/
    }

    @SuppressWarnings("rawtypes")
    synchronized public void unistallResource(Resource r, IntallationProgressListener progressListener) {
        AddOnManagerUtils.unload(r);

        if (r.getType().equals(TranslationData.class)) {
            translation.getCustomGroup().remove(r);
            List idList = props.getList("trans.custom");
            if (idList.contains(r.getId()))
                idList.remove(r.getId());
            props.setProperty("trans.custom", idList);
            saveConfig();
        } else if (r.getType().equals(AudioData.class)) {
            /*some task when un-installing recitations*/
        } else
            throw new InvalidParameterException("ResourceType not been implement yet");

        /*String configurationKey = "resources." + r.getType().getSimpleName();
        List idList = props.getList(configurationKey);
        if (idList.contains(r.getId()))
           idList.remove(r.getId());
        props.setProperty(configurationKey, idList);
        saveConfig();*/

        FileUtils.deleteQuietly(r.getFile());
        progressListener.finish(r);
    }
    // </patch>
}