2 * Copyright 2003 Sun Microsystems, Inc.
4 * See the file "license.terms" for information on usage and
5 * redistribution of this file, and for a DISCLAIMER OF ALL
8 package com.sun.speech.freetts;
10 import java.io.BufferedReader;
12 import java.io.FileInputStream;
13 import java.io.FileNotFoundException;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.InputStreamReader;
17 import java.net.JarURLConnection;
18 import java.net.MalformedURLException;
21 import java.net.URLClassLoader;
22 import java.util.Collection;
23 import java.util.Iterator;
24 import java.util.jar.Attributes;
27 * Provides access to voices for all of FreeTTS. There is only one instance of
30 * Each call to getVoices() creates a new instance of each voice.
35 public class VoiceManager {
37 private static final VoiceManager INSTANCE;
39 private static final String PATH_SEPARATOR ;
42 * we only want one class loader, otherwise the static information for
43 * loaded classes would be duplicated for each class loader
45 private static final DynamicClassLoader classLoader;
48 PATH_SEPARATOR = System.getProperty("path.separator");
49 INSTANCE = new VoiceManager();
50 final ClassLoader parent = VoiceManager.class.getClassLoader();
51 classLoader = new DynamicClassLoader(new URL[0], parent);
55 * Do not allow creation from outside.
57 private VoiceManager() {
61 * Gets the instance of the VoiceManager
63 * @return a VoiceManager
65 public static VoiceManager getInstance() {
70 * Provide an array of all voices available to FreeTTS.
72 * First, if the "freetts.voices" property is set, it is assumed to be a
73 * comma-separated list of VoiceDirectory classnames (e.g.,
74 * "-Dfreetts.voices=com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory"
75 * ). If this property exists, the VoiceManager will use only this property
76 * to find voices -- no other method described below will be used. The
77 * primary purpose for this property is testing and for use with WebStart.
80 * Second, the file internal_voices.txt is looked for in the same directory
81 * as VoiceManager.class. If the file does not exist, the VoiceManager moves
82 * on. Next, it looks for voices.txt in the same directory as freetts.jar.
83 * If the file does not exist, the VoiceManager moves on. Next, if the
84 * property "freetts.voicesfile" is defined, then that file is read in. If
85 * the property is defined and the file does not exist, then an error is
89 * Every voices file that is read in contains a list of VoiceDirectory class
93 * Next, the voice manager looks for freetts voice jarfiles that may exist
94 * in well-known locations. The directory that contains freetts.jar is
95 * searched for voice jarfiles, then directories specified by the
96 * "freetts.voicespath" system property. Any jarfile whose Manifest contains
97 * "FreeTTSVoiceDefinition: true" is assumed to be a FreeTTS voice, and the
98 * Manifest's "Main-Class" entry is assumed to be the name of the voice
99 * directory. The dependencies of the voice jarfiles specified by the
100 * "Class-Path" Manifest entry are also loaded.
103 * The VoiceManager instantiates each voice directory and calls getVoices()
106 * @return the array of new instances of all available voices
108 public Voice[] getVoices() {
109 UniqueVector voices = new UniqueVector();
110 Collection voiceDirectories = getVoiceDirectories();
111 Iterator iterator = voiceDirectories.iterator();
112 while (iterator.hasNext()) {
113 VoiceDirectory dir = (VoiceDirectory) iterator.next();
114 voices.addArray(dir.getVoices());
117 Voice[] voiceArray = new Voice[voices.size()];
118 return (Voice[]) voices.toArray(voiceArray);
122 * Prints detailed information about all available voices.
124 * @return a String containing the information
126 public String getVoiceInfo() {
127 String infoString = "";
128 Collection voiceDirectories = getVoiceDirectories();
129 Iterator iterator = voiceDirectories.iterator();
130 while (iterator.hasNext()) {
131 VoiceDirectory dir = (VoiceDirectory) iterator.next();
132 infoString += dir.toString();
138 * Creates an array of all voice directories of all available voices using
139 * the criteria specified by the contract for getVoices().
141 * @return the voice directories
144 private Collection getVoiceDirectories() {
146 // If there is a freetts.voices property, it means two
147 // things: 1) it is a comma separated list of class names
148 // 2) no other attempts to find voices should be
151 // The main purpose for this property is to allow for
152 // voices to be found via WebStart.
154 String voiceClasses = System.getProperty("freetts.voices");
155 if (voiceClasses != null) {
156 return getVoiceDirectoryNamesFromProperty(voiceClasses);
159 // Get voice directory names from voices files
160 UniqueVector voiceDirectoryNames = getVoiceDirectoryNamesFromFiles();
162 // Get list of voice jars
163 UniqueVector pathURLs = getVoiceJarURLs();
165 .addVector(getVoiceDirectoryNamesFromJarURLs(pathURLs));
168 // Copy of vector made because vector may be modified by
169 // each call to getDependencyURLs
170 URL[] voiceJarURLs = (URL[]) pathURLs.toArray(
171 new URL[pathURLs.size()]);
172 for (int i = 0; i < voiceJarURLs.length; i++) {
173 getDependencyURLs(voiceJarURLs[i], pathURLs);
176 // If the voice jars have already been added to the classpath
177 // we avoid to add them a second time.
178 boolean noexpansion = Boolean.getBoolean("freetts.nocpexpansion");
181 for (int i = 0; i < pathURLs.size(); i++) {
182 classLoader.addUniqueURL((URL) pathURLs.get(i));
186 // Create an instance of each voice directory
187 UniqueVector voiceDirectories = new UniqueVector();
188 for (int i = 0; i < voiceDirectoryNames.size(); i++) {
189 Class c = Class.forName((String) voiceDirectoryNames.get(i),
191 voiceDirectories.add(c.newInstance());
194 return voiceDirectories.elements();
195 } catch (InstantiationException e) {
196 throw new Error("Unable to load voice directory. " + e);
197 } catch (ClassNotFoundException e) {
198 throw new Error("Unable to load voice directory. " + e);
199 } catch (IllegalAccessException e) {
200 throw new Error("Unable to load voice directory. " + e);
206 * Gets VoiceDirectory instances by parsing a comma separated String of
207 * VoiceDirectory class names.
209 private Collection getVoiceDirectoryNamesFromProperty(
210 String voiceClasses) throws InstantiationException,
211 IllegalAccessException, ClassNotFoundException {
213 String[] classnames = voiceClasses.split(",");
215 Collection directories = new java.util.ArrayList();
217 for (int i = 0; i < classnames.length; i++) {
218 Class c = classLoader.loadClass(classnames[i]);
219 directories.add(c.newInstance());
226 * Recursively gets the urls of the class paths that url is dependant on.
228 * Conventions specified in
229 * http://java.sun.com/j2se/1.4.1/docs/guide/extensions/spec.html#bundled
233 * the url to recursively check. If it ends with a "/" then it is
234 * presumed to be a directory, and is not checked. Otherwise it
235 * is assumed to be a jar, and its manifest is read to get the
236 * urls Class-Path entry. These urls are passed to this method
239 * @param dependencyURLs
240 * a vector containing all of the dependant urls found. This
241 * parameter is modified as urls are added to it.
243 private void getDependencyURLs(URL url, UniqueVector dependencyURLs) {
245 String urlDirName = getURLDirName(url);
246 if (url.getProtocol().equals("jar")) { // only check deps of jars
248 // read in Class-Path attribute of jar Manifest
249 JarURLConnection jarConnection = (JarURLConnection) url
251 Attributes attributes = jarConnection.getMainAttributes();
252 String fullClassPath = attributes
253 .getValue(Attributes.Name.CLASS_PATH);
254 if (fullClassPath == null || fullClassPath.equals("")) {
255 return; // no classpaths to add
258 // The URLs are separated by one or more spaces
259 String[] classPath = fullClassPath.split("\\s+");
261 for (int i = 0; i < classPath.length; i++) {
263 if (classPath[i].endsWith("/")) { // assume directory
264 classPathURL = new URL("file:" + urlDirName
266 } else { // assume jar
267 classPathURL = new URL("jar", "", "file:"
268 + urlDirName + classPath[i] + "!/");
270 } catch (MalformedURLException e) {
272 .println("Warning: unable to resolve dependency "
279 // don't get in a recursive loop if two jars
280 // are mutually dependant
281 if (!dependencyURLs.contains(classPathURL)) {
282 dependencyURLs.add(classPathURL);
283 getDependencyURLs(classPathURL, dependencyURLs);
287 } catch (IOException e) {
293 * Gets the names of the subclasses of VoiceDirectory that are listed in the
296 * @return a vector containing the String names of the voice directories
298 private UniqueVector getVoiceDirectoryNamesFromFiles() {
300 UniqueVector voiceDirectoryNames = new UniqueVector();
302 // first, load internal_voices.txt
303 InputStream is = this.getClass().getResourceAsStream(
304 "internal_voices.txt");
305 if (is != null) { // if it doesn't exist, move on
307 .addVector(getVoiceDirectoryNamesFromInputStream(is));
310 // next, try loading voices.txt
313 .addVector(getVoiceDirectoryNamesFromFile(getBaseDirectory()
315 } catch (FileNotFoundException e) {
317 } catch (IOException e) {
321 // last, read voices from property freetts.voicesfile
322 String voicesFile = System.getProperty("freetts.voicesfile");
323 if (voicesFile != null) {
325 .addVector(getVoiceDirectoryNamesFromFile(voicesFile));
328 return voiceDirectoryNames;
329 } catch (IOException e) {
330 throw new Error("Error reading voices files. " + e);
335 * Gets the voice directory class names from a list of urls specifying voice
336 * jarfiles. The class name is specified as the Main-Class in the manifest
340 * a UniqueVector of URLs that refer to the voice jarfiles
342 * @return a UniqueVector of Strings representing the voice directory class
345 private UniqueVector getVoiceDirectoryNamesFromJarURLs(UniqueVector urls) {
347 UniqueVector voiceDirectoryNames = new UniqueVector();
348 for (int i = 0; i < urls.size(); i++) {
349 JarURLConnection jarConnection = (JarURLConnection) ((URL) urls
350 .get(i)).openConnection();
351 Attributes attributes = jarConnection.getMainAttributes();
352 String mainClass = attributes
353 .getValue(Attributes.Name.MAIN_CLASS);
354 if (mainClass == null || mainClass.trim().equals("")) {
355 throw new Error("No Main-Class found in jar "
356 + (URL) urls.get(i));
359 voiceDirectoryNames.add(mainClass);
361 return voiceDirectoryNames;
362 } catch (IOException e) {
363 throw new Error("Error reading jarfile manifests. ");
368 * Gets the list of voice jarfiles. Voice jarfiles are searched for in the
369 * same directory as freetts.jar and the directories specified by the
370 * freetts.voicespath system property. Voice jarfiles are defined by the
371 * manifest entry "FreeTTSVoiceDefinition: true"
373 * @return a vector of URLs refering to the voice jarfiles.
375 private UniqueVector getVoiceJarURLs() {
376 UniqueVector voiceJarURLs = new UniqueVector();
378 // check in same directory as freetts.jar
380 String baseDirectory = getBaseDirectory();
381 if (!baseDirectory.equals("")) { // not called from a jar
382 voiceJarURLs.addVector(getVoiceJarURLsFromDir(baseDirectory));
384 } catch (FileNotFoundException e) {
389 String voicesPath = System.getProperty("freetts.voicespath", "");
390 if (!voicesPath.equals("")) {
391 String[] dirNames = voicesPath.split(PATH_SEPARATOR);
392 for (int i = 0; i < dirNames.length; i++) {
394 voiceJarURLs.addVector(getVoiceJarURLsFromDir(dirNames[i]));
395 } catch (FileNotFoundException e) {
396 throw new Error("Error loading jars from voicespath "
397 + dirNames[i] + ". ");
406 * Gets the list of voice jarfiles in a specific directory.
408 * @return a vector of URLs refering to the voice jarfiles
409 * @see getVoiceJarURLs()
411 private UniqueVector getVoiceJarURLsFromDir(String dirName)
412 throws FileNotFoundException {
414 UniqueVector voiceJarURLs = new UniqueVector();
415 File dir = new File(new URI("file://" + dirName));
416 if (!dir.isDirectory()) {
417 throw new FileNotFoundException("File is not a directory: "
420 File[] files = dir.listFiles();
421 for (int i = 0; i < files.length; i++) {
422 File file = files[i];
423 if (file.isFile() && (!file.isHidden())
424 && file.getName().endsWith(".jar")) {
425 URL jarURL = file.toURI().toURL();
426 jarURL = new URL("jar", "", "file:" + jarURL.getPath()
428 JarURLConnection jarConnection = (JarURLConnection) jarURL
430 // if it is not a real jar file, we will end up
431 // with a null set of attributes.
433 Attributes attributes = jarConnection.getMainAttributes();
434 if (attributes != null) {
435 String isVoice = attributes
436 .getValue("FreeTTSVoiceDefinition");
437 if (isVoice != null && isVoice.trim().equals("true")) {
438 voiceJarURLs.add(jarURL);
444 } catch (java.net.URISyntaxException e) {
445 throw new Error("Error reading directory name '" + dirName + "'.");
446 } catch (MalformedURLException e) {
447 throw new Error("Error reading jars from directory " + dirName
449 } catch (IOException e) {
450 throw new Error("Error reading jars from directory " + dirName
456 * Provides a string representation of all voices available to FreeTTS.
458 * @return a String which is a space-delimited list of voice names. If there
459 * is more than one voice, then the word "or" appears before the
462 public String toString() {
464 Voice[] voices = getVoices();
465 for (int i = 0; i < voices.length; i++) {
466 if (i == voices.length - 1) {
468 names = voices[i].getName();
470 names += "or " + voices[i].getName();
473 names += voices[i].getName() + " ";
480 * Check if there is a voice provides with the given name.
483 * the name of the voice to check
485 * @return <b>true</b> if FreeTTS has a voice available with the name
486 * <b>voiceName</b>, else <b>false</b>.
488 public boolean contains(String voiceName) {
489 return (getVoice(voiceName) != null);
493 * Get a Voice with a given name.
496 * the name of the voice to get.
498 * @return the Voice that has the same name as <b>voiceName</b> if one
499 * exists, else <b>null</b>
501 public Voice getVoice(String voiceName) {
502 Voice[] voices = getVoices();
503 for (int i = 0; i < voices.length; i++) {
504 if (voices[i].getName().equals(voiceName)) {
512 * Get the directory that the jar file containing this class resides in.
514 * @return the name of the directory with a trailing "/" (or equivalent for
515 * the particular operating system), or "" if unable to determin.
516 * (For example this class does not reside inside a jar file).
518 private String getBaseDirectory() {
519 String name = this.getClass().getName();
520 int lastdot = name.lastIndexOf('.');
521 if (lastdot != -1) { // remove package information
522 name = name.substring(lastdot + 1);
525 URL url = this.getClass().getResource(name + ".class");
526 return getURLDirName(url);
530 * Gets the directory name from a URL
534 * @return the String representation of the directory name in a URL
536 private String getURLDirName(URL url) {
537 String urlFileName = url.getPath();
538 int i = urlFileName.lastIndexOf('!');
540 i = urlFileName.length();
542 int dir = urlFileName.lastIndexOf("/", i);
543 if (!urlFileName.startsWith("file:")) {
546 return urlFileName.substring(5, dir) + "/";
550 * Get the names of the voice directories from a voices file. Blank lines
551 * and lines beginning with "#" are ignored. Beginning and trailing
552 * whitespace is ignored.
555 * the name of the voices file to read from
557 * @return a vector of the names of the VoiceDirectory subclasses
558 * @throws FileNotFoundException
559 * @throws IOException
561 private UniqueVector getVoiceDirectoryNamesFromFile(String fileName)
562 throws FileNotFoundException, IOException {
563 InputStream is = new FileInputStream(fileName);
565 throw new IOException();
567 return getVoiceDirectoryNamesFromInputStream(is);
572 * Get the names of the voice directories from an input stream. Blank lines
573 * and lines beginning with "#" are ignored. Beginning and trailing
574 * whitespace is ignored.
577 * the input stream to read from
579 * @return a vector of the names of the VoiceDirectory subclasses
580 * @throws IOException
582 private UniqueVector getVoiceDirectoryNamesFromInputStream(InputStream is)
584 UniqueVector names = new UniqueVector();
585 BufferedReader reader = new BufferedReader(new InputStreamReader(is));
587 String line = reader.readLine();
592 if (!line.startsWith("#") && !line.equals("")) {
600 * Gets the class loader used for loading dynamically detected jars. This is
601 * useful to get resources out of jars that may be in the class path of this
602 * class loader but not in the class path of the system class loader.
604 * @return the class loader
606 public static URLClassLoader getVoiceClassLoader() {
612 * The DynamicClassLoader provides a means to add urls to the classpath after
613 * the class loader has already been instantiated.
615 class DynamicClassLoader extends URLClassLoader {
617 private java.util.HashSet classPath;
620 * Constructs a new URLClassLoader for the given URLs. The URLs will be
621 * searched in the order specified for classes and resources after first
622 * searching in the specified parent class loader. Any URL that ends with a
623 * '/' is assumed to refer to a directory. Otherwise, the URL is assumed to
624 * refer to a JAR file which will be downloaded and opened as needed.
626 * If there is a security manager, this method first calls the security
627 * manager's checkCreateClassLoader method to ensure creation of a class
631 * the URLs from which to load classes and resources
633 * the parent class loader for delegation
635 * @throws SecurityException
636 * if a security manager exists and its checkCreateClassLoader
637 * method doesn't allow creation of a class loader.
639 public DynamicClassLoader(URL[] urls, ClassLoader parent) {
641 classPath = new java.util.HashSet(urls.length);
642 for (int i = 0; i < urls.length; i++) {
643 classPath.add(urls[i]);
648 * Add a URL to a class path only if has not already been added.
651 * the url to add to the class path
653 public synchronized void addUniqueURL(final URL url) {
654 // Avoid loading of the freetts.jar.
655 final String name= url.toString();
656 if (!classPath.contains(url) && (name.indexOf("freetts.jar") < 0)) {
665 public Class loadClass(final String name)
666 throws ClassNotFoundException {
667 Class loadedClass = findLoadedClass(name);
668 if (loadedClass == null) {
670 loadedClass = findClass(name);
671 } catch (ClassNotFoundException e) {
673 // does not exist locally
675 if (loadedClass == null) {
676 loadedClass = super.loadClass(name);
684 * Provides a vector whose elements are always unique. The advantage over a Set
685 * is that the elements are still ordered in the way they were added. If an
686 * element is added that already exists, then nothing happens.
689 private java.util.HashSet elementSet;
690 private java.util.Vector elementVector;
693 * Creates a new vector
695 public UniqueVector() {
696 elementSet = new java.util.HashSet();
697 elementVector = new java.util.Vector();
701 * Add an object o to the vector if it is not already present as defined by
702 * the function HashSet.contains(o)
707 public void add(Object o) {
710 elementVector.add(o);
715 * Appends all elements of a vector to this vector. Only unique elements are
721 public void addVector(UniqueVector v) {
722 for (int i = 0; i < v.size(); i++) {
728 * Appends all elements of an array to this vector. Only unique elements are
734 public void addArray(Object[] a) {
735 for (int i = 0; i < a.length; i++) {
741 * Gets the number of elements currently in vector.
743 * @return the number of elements in vector
746 return elementVector.size();
750 * Checks if an element is present in the vector. The check follows the
751 * convention of HashSet contains() function, so performance can be expected
752 * to be a constant factor.
755 * the object to check
757 * @return true if element o exists in the vector, else false.
759 public boolean contains(Object o) {
760 return elementSet.contains(o);
764 * Gets an element from a vector.
767 * the index into the vector from which to retrieve the element
769 * @return the object at index <b>index</b>
771 public Object get(int index) {
772 return elementVector.get(index);
776 * Creates an array of the elements in the vector. Follows conventions of
779 * @return an array representation of the object
781 public Object[] toArray() {
782 return elementVector.toArray();
786 * Creates an array of the elements in the vector. Follows conventions of
787 * Vector.toArray(Object[]).
789 * @return an array representation of the object
791 public Object[] toArray(Object[] a) {
792 return elementVector.toArray(a);
796 * Returns the entries of this vector.
799 public Collection elements() {
800 return elementVector;