marytts.modules.ModuleRegistry.java Source code

Java tutorial

Introduction

Here is the source code for marytts.modules.ModuleRegistry.java

Source

/**
 * Copyright 2008 DFKI GmbH.
 * All Rights Reserved.  Use is subject to license terms.
 *
 * This file is part of MARY TTS.
 *
 * MARY TTS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package marytts.modules;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import marytts.datatypes.MaryDataType;
import marytts.exceptions.MaryConfigurationException;
import marytts.modules.synthesis.Voice;
import marytts.util.MaryRuntimeUtils;
import marytts.util.MaryUtils;

import org.apache.commons.collections.map.MultiKeyMap;
import org.apache.log4j.Logger;

/**
 * A hierarchical repository for Mary modules,
 * allowing the flexible indexing by an ordered hierarchy of datatype, locale and voice.
 * A given lookup will search for a combination of datatype, locale and voice
 * first; if it does not find a value, it will look for datatype, locale, and null;
 * if it does notfind that, it will look for datatype, null, and null.
 * 
 * @author marc
 *
 */
public class ModuleRegistry {
    private static MultiKeyMap mkm;
    private static List<MaryModule> allModules;
    private static boolean registrationComplete;
    private static Logger logger;

    private ModuleRegistry() {
    }

    /**
     * Create a new, empty module repository.
     */
    static {
        mkm = new MultiKeyMap();
        allModules = new LinkedList<MaryModule>();
        registrationComplete = false;
        logger = MaryUtils.getLogger("ModuleRegistry");
    }

    //////////////////////////////////////////////////////////////////
    ///////////////////////// instantiation //////////////////////////
    //////////////////////////////////////////////////////////////////

    /**
     * From the given module init info, instantiate a new mary module.
     * @param moduleInitInfo a string description of the module to instantiate.
     * The moduleInitInfo is expected to have one of the following forms:
     * <ol>
     *   <li> my.class.which.extends.MaryModule</li>
     *   <li> my.class.which.extends.MaryModule(any,string,args,without,spaces)</li>
     *   <li>my.class.which.extends.MaryModule(arguments,$my.special.property,other,args)</li>
     * </ol>
     * where 'my.special.property' is a property in the property file.
     * @throws MaryConfigurationException if the module cannot be instantiated
     */
    public static MaryModule instantiateModule(String moduleInitInfo) throws MaryConfigurationException {
        logger.info("Now initiating mary module '" + moduleInitInfo + "'");
        MaryModule m = (MaryModule) MaryRuntimeUtils.instantiateObject(moduleInitInfo);
        return m;
    }

    //////////////////////////////////////////////////////////////////
    ///////////////////////// registration ///////////////////////////
    //////////////////////////////////////////////////////////////////

    /**
     * Register a MaryModule as an appropriate module to process the given
     * combination of MaryDataType for the input data, locale of the input data,
     * and voice requested for processing. 
     * Note that it is possible to register more than one module for a given
     * combination of input type, locale and voice; in that case, all of them
     * will be remembered, and will be returned as a List<MaryModule> by get().
     * 
     * @param module the module to add to the registry, under its input type and the given locale and voice.
     * @param locale the locale (language or language-COUNTRY) of the input data; can be null to signal that
     * the module is locale-independent.
     * @param voice a voice for which this module is suited. Can be null to indicate
     * that the module is not specific to any voice.
     * @throws IllegalStateException if called after registration is complete. 
     */
    @SuppressWarnings("unchecked")
    public static void registerModule(MaryModule module, Locale locale, Voice voice) throws IllegalStateException {
        if (registrationComplete)
            throw new IllegalStateException("cannot register modules after registration is complete");
        MaryDataType type = module.inputType();
        Object o = mkm.get(type, locale, voice);
        List<MaryModule> l;
        if (o != null) {
            assert o instanceof List : "Expected List of MaryModules, got " + o.getClass();
            l = (List<MaryModule>) o;
        } else {
            l = new ArrayList<MaryModule>(1);
            mkm.put(type, locale, voice, l);
        }
        assert l != null;
        l.add(module);

        allModules.add(module);

        MaryDataType.registerDataType(type);
        MaryDataType.registerDataType(module.outputType());
    }

    /**
     * Determine whether or not the registration is complete.
     * When the registration is not (yet) complete, calls to 
     * @see #registerModule(MaryModule, Locale, Voice) are possible;
     * when the registration is complete, calls to the other methods are possible.
     *  
     * @return false when the registration is still open, true when it is complete.
     */
    public static boolean getRegistrationComplete() {
        return registrationComplete;
    }

    /**
     * Indicate that the registration is now complete. No further calls to
     * registerModules() will be possible.
     * @throws IllegalStateException if called when registration was already completed before.
     */
    public static void setRegistrationComplete() throws IllegalStateException {
        if (registrationComplete)
            throw new IllegalStateException("Registration has already completed, cannot do that a second time");

        registrationComplete = true;
        MaryDataType.setRegistrationComplete();
    }

    //////////////////////////////////////////////////////////////////
    /////////////////////////  modules   /////////////////////////////
    //////////////////////////////////////////////////////////////////

    /**
     * Provide a list containing all MaryModules instances.
     * The order is not important.
     * @throws IllegalStateException if called while registration is not yet complete. 
     * 
     */
    public static List<MaryModule> getAllModules() {
        if (!registrationComplete)
            throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
        return Collections.unmodifiableList(allModules);
    }

    /**
     * Find an active module by its class.
     * @return the module instance if found, or null if not found.
     * @throws IllegalStateException if called while registration is not yet complete. 
     */
    // TODO: what should happen with this method when we parameterise modules, so that there can be several instances of the same class?
    public static MaryModule getModule(Class<?> moduleClass) {
        if (!registrationComplete)
            throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
        for (Iterator<MaryModule> it = allModules.iterator(); it.hasNext();) {
            MaryModule m = it.next();
            if (m.getClass().equals(moduleClass)) {
                return m;
            }
        }
        // Not found:
        return null;
    }

    /**
     * A method for determining the list of modules required to transform
     * the given source data type into the requested target data type.
     * @return the (ordered) list of modules required, or null if no such
     * list could be found.
     * @throws IllegalStateException if called while registration is not yet complete. 
     * @throws NullPointerException if source data type, target data type or locale is null.
     */
    public static LinkedList<MaryModule> modulesRequiredForProcessing(MaryDataType sourceType,
            MaryDataType targetType, Locale locale) {
        return modulesRequiredForProcessing(sourceType, targetType, locale, null);
    }

    /**
     * A method for determining the list of modules required to transform
     * the given source data type into the requested target data type. If the
     * voice given is not null, any preferred modules it may have are taken into account.
     * @return the (ordered) list of modules required, or null if no such
     * list could be found.
     * @throws IllegalStateException if called while registration is not yet complete. 
     * @throws NullPointerException if source data type, target data type or locale is null.
     */
    public static LinkedList<MaryModule> modulesRequiredForProcessing(MaryDataType sourceType,
            MaryDataType targetType, Locale locale, Voice voice) {
        if (!registrationComplete)
            throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
        if (sourceType == null)
            throw new NullPointerException("Received null source type");
        if (targetType == null)
            throw new NullPointerException("Received null target type");
        //if (locale == null)
        //    throw new NullPointerException("Received null locale");
        LinkedList<MaryDataType> seenTypes = new LinkedList<MaryDataType>();
        seenTypes.add(sourceType);
        return modulesRequiredForProcessing(sourceType, targetType, locale, voice, seenTypes);
    }

    /**
     * This method recursively calls itself.
     * It forward-constructs a list of seen types (upon test),
     * and backward-constructs a list of required modules (upon success).
     *
     * @param sourceType
     * @param targetType
     * @param locale
     * @param voice
     * @param seenTypes
     * @return
     */
    private static LinkedList<MaryModule> modulesRequiredForProcessing(MaryDataType sourceType,
            MaryDataType targetType, Locale locale, Voice voice, LinkedList<MaryDataType> seenTypes) {
        // Terminating condition:
        if (sourceType.equals(targetType)) {
            logger.debug("found path through modules");
            return new LinkedList<MaryModule>();
        }
        // Recursion step:
        // Any voice-specific modules?
        List<MaryModule> candidates = null;
        // TODO: the following should be obsolete as soon as we are properly using the voice index in ModuleRegistry
        if (voice != null)
            candidates = voice.getPreferredModulesAcceptingType(sourceType);
        if (candidates == null || candidates.isEmpty()) { // default: use all available modules
            candidates = get(sourceType, locale, voice);
        }
        if (candidates == null || candidates.isEmpty()) {
            // no module can handle this type
            return null;
        }
        for (Iterator<MaryModule> it = candidates.iterator(); it.hasNext();) {
            MaryModule candidate = it.next();
            MaryDataType outputType = candidate.outputType();
            // Ignore candidates that would bring us to a data type that we
            // have already seen (i.e., that would lead to a loop):
            if (!seenTypes.contains(outputType)) {
                seenTypes.add(outputType);
                logger.debug("Module " + candidate.name() + " converts " + sourceType.name() + " into " + outputType
                        + " (locale " + locale + ", voice " + voice + ")");
                // recursive call:
                LinkedList<MaryModule> path = modulesRequiredForProcessing(outputType, targetType, locale, voice,
                        seenTypes);
                if (path != null) {
                    // success, found a path of which candidate is the first
                    // step
                    path.addFirst(candidate);
                    return path;
                } // else, try next candidate
                seenTypes.removeLast();
            }
        }
        // We get here only if none of the candidates lead to a valid path
        return null; // failure
    }

    /**
     * Lookup a list of modules for the given combination of type, locale and voice.
     * A given lookup will return a list of modules accepting the datatype as input,
     * ordered by specificity so that the most specific modules come first.
     * For each output type produced by modules, there will be only one module
     * producing that type, namely the most specific one found.
     * Specificity is ordered as follows:
     * 1. most specific is the full combination of datatype, locale and voice;
     * 2. the combination of datatype, language-only locale and voice (i.e., for requested locale "en-US" will find modules with locale "en");
     * 3. the combination of datatype and locale, ignoring voice;
     * 4. the combination of datatype and language-only locale, ignoring voice;
     * 5. least specific is the datatype, ignoring locale and voice.
     * @param type the type of input data
     * @param locale the locale
     * @param voice the voice to use.
     * @return a list of mary modules accepting the datatype and suitable for
     * the given locale and voice, ordered by their specificity,
     * or an empty list if there is no suitable module.
     * @throws IllegalStateException if called when registration is not yet complete. 
     */
    @SuppressWarnings("unchecked")
    private static List<MaryModule> get(MaryDataType type, Locale locale, Voice voice)
            throws IllegalStateException {
        if (!registrationComplete)
            throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
        LinkedHashMap<MaryDataType, MaryModule> results = new LinkedHashMap<MaryDataType, MaryModule>();
        // First, get all results:
        List<List<MaryModule>> listOfLists = new LinkedList<List<MaryModule>>();
        listOfLists.add((List<MaryModule>) mkm.get(type, locale, voice));
        Locale langOnly = locale != null ? new Locale(locale.getLanguage()) : null;
        boolean haveCountry = langOnly == null ? false : !langOnly.equals(locale);
        if (haveCountry) {
            listOfLists.add((List<MaryModule>) mkm.get(type, langOnly, voice));
        }
        listOfLists.add((List<MaryModule>) mkm.get(type, locale, null));
        if (haveCountry) {
            listOfLists.add((List<MaryModule>) mkm.get(type, langOnly, null));
        }
        listOfLists.add((List<MaryModule>) mkm.get(type, null, null));
        // Now, for each mary output type, keep only the most specific module,
        // and return the list ordered by the specificity of modules (most specific first):
        for (List<MaryModule> list : listOfLists) {
            if (list != null) {
                for (MaryModule m : list) {
                    if (!results.containsKey(m.outputType()))
                        results.put(m.outputType(), m);
                }
            }
        }
        List<MaryModule> returnList = new LinkedList<MaryModule>();
        for (MaryDataType t : results.keySet()) {
            returnList.add(results.get(t));
        }
        return returnList;
    }

}