001/* MailcapCommandMap.java -- Command map implementation using a mailcap file.
002   Copyright (C) 2004 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038package javax.activation;
039
040import gnu.java.lang.CPStringBuilder;
041
042import java.io.BufferedReader;
043import java.io.File;
044import java.io.FileReader;
045import java.io.InputStream;
046import java.io.InputStreamReader;
047import java.io.IOException;
048import java.io.Reader;
049import java.io.StringReader;
050import java.net.URL;
051import java.util.ArrayList;
052import java.util.Enumeration;
053import java.util.LinkedHashMap;
054import java.util.Iterator;
055import java.util.List;
056import java.util.Map;
057
058/**
059 * Implementation of a command map using a <code>mailcap</code> file (RFC
060 * 1524). Mailcap files are searched for in the following places:
061 * <ol>
062 * <li>Programmatically added entries to this interface</li>
063 * <li>the file <tt>.mailcap</tt> in the user's home directory</li>
064 * <li>the file <i>&lt;java.home&gt;</i><tt>/lib/mailcap</tt></li>
065 * <li>the resource <tt>META-INF/mailcap</tt></li>
066 * <li>the resource <tt>META-INF/mailcap.default</tt> in the JAF
067 * distribution</li>
068 * </ol>
069 *
070 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
071 * @version 1.1
072 */
073public class MailcapCommandMap
074    extends CommandMap
075{
076
077  private static final int PROG = 0;
078  private static final int HOME = 1;
079  private static final int SYS = 2;
080  private static final int JAR = 3;
081  private static final int DEF = 4;
082  private static boolean debug = false;
083  private static final int NORMAL = 0;
084  private static final int FALLBACK = 1;
085
086  static
087  {
088    try
089      {
090        String d = System.getProperty("javax.activation.debug");
091        debug = Boolean.valueOf(d).booleanValue();
092      }
093    catch (SecurityException e)
094      {
095      }
096  }
097
098  private Map<String,Map<String,List<String>>>[][] mailcaps;
099
100  /**
101   * Default constructor.
102   */
103  public MailcapCommandMap()
104  {
105    init(null);
106  }
107
108  /**
109   * Constructor specifying a filename.
110   * @param fileName the name of the file to read mailcap entries from
111   */
112  public MailcapCommandMap(String fileName)
113    throws IOException
114  {
115    Reader in = null;
116    try
117      {
118        in = new FileReader(fileName);
119      }
120    catch (IOException e)
121      {
122      }
123    init(in);
124    if (in != null)
125      {
126        try
127          {
128            in.close();
129          }
130        catch (IOException e)
131          {
132          }
133      }
134  }
135
136  /**
137   * Constructor specifying an input stream.
138   * @param is the input stream to read mailcap entries from
139   */
140  public MailcapCommandMap(InputStream is)
141  {
142    init(new InputStreamReader(is));
143  }
144
145  private void init(Reader in)
146  {
147    mailcaps = new Map[5][2];
148    for (int i = 0; i < 5; i++)
149      {
150        for (int j = 0; j < 2; j++)
151          {
152            mailcaps[i][j] =
153              new LinkedHashMap<String,Map<String,List<String>>>();
154          }
155      }
156    if (in != null)
157      {
158        if (debug)
159          {
160            System.out.println("MailcapCommandMap: load PROG");
161          }
162        try
163          {
164            parse(PROG, in);
165          }
166        catch (IOException e)
167          {
168          }
169      }
170
171    if (debug)
172      {
173        System.out.println("MailcapCommandMap: load HOME");
174      }
175    try
176      {
177        String home = System.getProperty("user.home");
178        if (home != null)
179          {
180            parseFile(HOME, new CPStringBuilder(home)
181                      .append(File.separatorChar)
182                      .append(".mailcap")
183                      .toString());
184          }
185      }
186    catch (SecurityException e)
187      {
188      }
189
190    if (debug)
191      {
192        System.out.println("MailcapCommandMap: load SYS");
193      }
194    try
195      {
196        parseFile(SYS,
197                  new CPStringBuilder(System.getProperty("java.home"))
198                  .append(File.separatorChar)
199                  .append("lib")
200                  .append(File.separatorChar)
201                  .append("mailcap")
202                  .toString());
203      }
204    catch (SecurityException e)
205      {
206      }
207
208    if (debug)
209      {
210        System.out.println("MailcapCommandMap: load JAR");
211      }
212    List<URL> systemResources = getSystemResources("META-INF/mailcap");
213    int len = systemResources.size();
214    if (len > 0)
215      {
216        for (int i = 0; i < len ; i++)
217          {
218            Reader urlIn = null;
219            URL url = systemResources.get(i);
220            try
221              {
222                if (debug)
223                  {
224                    System.out.println("\t" + url.toString());
225                  }
226                urlIn = new InputStreamReader(url.openStream());
227                parse(JAR, urlIn);
228              }
229            catch (IOException e)
230              {
231                if (debug)
232                  {
233                    System.out.println(e.getClass().getName() + ": " +
234                                       e.getMessage());
235                  }
236              }
237            finally
238              {
239                if (urlIn != null)
240                  {
241                    try
242                      {
243                        urlIn.close();
244                      }
245                    catch (IOException e)
246                      {
247                      }
248                  }
249              }
250          }
251      }
252    else
253      {
254        parseResource(JAR, "/META-INF/mailcap");
255      }
256
257    if (debug)
258      {
259        System.out.println("MailcapCommandMap: load DEF");
260      }
261    parseResource(DEF, "/META-INF/mailcap.default");
262  }
263
264  /**
265   * Returns the list of preferred commands for a given MIME type.
266   * @param mimeType the MIME type
267   */
268  public synchronized CommandInfo[] getPreferredCommands(String mimeType)
269  {
270    List<CommandInfo> cmdList = new ArrayList<CommandInfo>();
271    List<String> verbList = new ArrayList<String>();
272    for (int i = 0; i < 2; i++)
273      {
274        for (int j = 0; j < 5; j++)
275          {
276            Map<String,List<String>> map = getCommands(mailcaps[j][i], mimeType);
277            if (map != null)
278              {
279                for (Map.Entry<String,List<String>> entry : map.entrySet())
280                  {
281                    String verb = entry.getKey();
282                    if (!verbList.contains(verb))
283                      {
284                        List<String> classNames = entry.getValue();
285                        String className = classNames.get(0);
286                        CommandInfo cmd = new CommandInfo(verb, className);
287                        cmdList.add(cmd);
288                        verbList.add(verb);
289                      }
290                  }
291              }
292          }
293      }
294    CommandInfo[] cmds = new CommandInfo[cmdList.size()];
295    cmdList.toArray(cmds);
296    return cmds;
297  }
298
299  /**
300   * Returns all commands for the given MIME type.
301   * @param mimeType the MIME type
302   */
303  public synchronized CommandInfo[] getAllCommands(String mimeType)
304  {
305    List<CommandInfo> cmdList = new ArrayList<CommandInfo>();
306    for (int i = 0; i < 2; i++)
307      {
308        for (int j = 0; j < 5; j++)
309          {
310            Map<String,List<String>> map = getCommands(mailcaps[j][i], mimeType);
311            if (map != null)
312              {
313                for (Map.Entry<String,List<String>> entry : map.entrySet())
314                  {
315                    String verb = entry.getKey();
316                    List<String> classNames = entry.getValue();
317                    int len = classNames.size();
318                    for (int l = 0; l < len; l++)
319                      {
320                        String className = classNames.get(l);
321                        CommandInfo cmd = new CommandInfo(verb, className);
322                        cmdList.add(cmd);
323                      }
324                  }
325              }
326          }
327      }
328    CommandInfo[] cmds = new CommandInfo[cmdList.size()];
329    cmdList.toArray(cmds);
330    return cmds;
331  }
332
333  /**
334   * Returns the command with the specified name for the given MIME type.
335   * @param mimeType the MIME type
336   * @param cmdName the command verb
337   */
338  public synchronized CommandInfo getCommand(String mimeType,
339                                             String cmdName)
340  {
341    for (int i = 0; i < 2; i++)
342      {
343        for (int j = 0; j < 5; j++)
344          {
345            Map<String,List<String>> map =
346              getCommands(mailcaps[j][i], mimeType);
347            if (map != null)
348              {
349                List<String> classNames = map.get(cmdName);
350                if (classNames == null)
351                  {
352                    classNames = map.get("x-java-" + cmdName);
353                  }
354                if (classNames != null)
355                  {
356                    String className = classNames.get(0);
357                    return new CommandInfo(cmdName, className);
358                  }
359              }
360          }
361      }
362    return null;
363  }
364
365  /**
366   * Adds entries programmatically to the registry.
367   * @param mailcap a mailcap string
368   */
369  public synchronized void addMailcap(String mailcap)
370  {
371    if (debug)
372      {
373        System.out.println("MailcapCommandMap: add to PROG");
374      }
375    try
376      {
377        parse(PROG, new StringReader(mailcap));
378      }
379    catch (IOException e)
380      {
381      }
382  }
383
384  /**
385   * Returns the DCH for the specified MIME type.
386   * @param mimeType the MIME type
387   */
388  public synchronized DataContentHandler
389    createDataContentHandler(String mimeType)
390  {
391    if (debug)
392      {
393        System.out.println("MailcapCommandMap: " +
394                           "createDataContentHandler for " + mimeType);
395      }
396    for (int i = 0; i < 2; i++)
397      {
398        for (int j = 0; j < 5; j++)
399          {
400            if (debug)
401              {
402                System.out.println("  search DB #" + i);
403              }
404            Map<String,List<String>> map = getCommands(mailcaps[j][i], mimeType);
405            if (map != null)
406              {
407                List<String> classNames = map.get("content-handler");
408                if (classNames == null)
409                  {
410                    classNames = map.get("x-java-content-handler");
411                  }
412                if (classNames != null)
413                  {
414                    String className = classNames.get(0);
415                    if (debug)
416                      {
417                        System.out.println("  In " + nameOf(j) +
418                                           ", content-handler=" + className);
419                      }
420                    try
421                      {
422                        Class<?> clazz = Class.forName(className);
423                        return (DataContentHandler)clazz.newInstance();
424                      }
425                    catch (IllegalAccessException e)
426                      {
427                        if (debug)
428                          {
429                            e.printStackTrace();
430                          }
431                      }
432                    catch (ClassNotFoundException e)
433                      {
434                        if (debug)
435                      {
436                        e.printStackTrace();
437                      }
438                      }
439                    catch (InstantiationException e)
440                      {
441                        if (debug)
442                          {
443                            e.printStackTrace();
444                          }
445                      }
446                  }
447              }
448          }
449      }
450    return null;
451  }
452
453  /**
454   * Get the native commands for the given MIME type.
455   * Returns an array of strings where each string is
456   * an entire mailcap file entry.  The application
457   * will need to parse the entry to extract the actual
458   * command as well as any attributes it needs. See
459   * <a href="http://www.ietf.org/rfc/rfc1524.txt">RFC 1524</a>
460   * for details of the mailcap entry syntax.  Only mailcap
461   * entries that specify a view command for the specified
462   * MIME type are returned.
463   * @return array of native command entries
464   * @since JAF 1.1
465   */
466  public String[] getNativeCommands(String mimeType)
467  {
468    List<String> acc = new ArrayList<String>();
469    for (int i = 0; i < 2; i++)
470      {
471        for (int j = 0; j < 5; j++)
472          {
473            addNativeCommands(acc, mailcaps[j][i], mimeType);
474          }
475      }
476    String[] ret = new String[acc.size()];
477    acc.toArray(ret);
478    return ret;
479  }
480
481  private void addNativeCommands(List<String> acc,
482                                 Map<String,Map<String,List<String>>> mailcap,
483                                 String mimeType)
484  {
485    for (Map.Entry<String,Map<String,List<String>>> mEntry : mailcap.entrySet())
486      {
487        String entryMimeType = mEntry.getKey();
488        if (!entryMimeType.equals(mimeType))
489          {
490            continue;
491          }
492        Map<String,List<String>> commands = mEntry.getValue();
493        String viewCommand = commands.get("view-command").get(0);
494        if (viewCommand == null)
495          {
496            continue;
497          }
498        CPStringBuilder buf = new CPStringBuilder();
499        buf.append(mimeType);
500        buf.append(';');
501        buf.append(' ');
502        buf.append(viewCommand);
503        for (Map.Entry<String,List<String>> cEntry : commands.entrySet())
504          {
505            String verb = cEntry.getKey();
506            List<String> classNames = cEntry.getValue();
507            if (!"view-command".equals(verb))
508              {
509                for (String command : classNames)
510                  {
511                    buf.append(';');
512                    buf.append(' ');
513                    buf.append(verb);
514                    buf.append('=');
515                    buf.append(command);
516                  }
517              }
518          }
519        if (buf.length() > 0)
520          {
521            acc.add(buf.toString());
522          }
523      }
524  }
525
526  private static String nameOf(int mailcap)
527  {
528    switch (mailcap)
529      {
530      case PROG:
531        return "PROG";
532      case HOME:
533        return "HOME";
534      case SYS:
535        return "SYS";
536      case JAR:
537        return "JAR";
538      case DEF:
539        return "DEF";
540      default:
541        return "ERR";
542      }
543  }
544
545  private void parseFile(int index, String filename)
546  {
547    Reader in = null;
548    try
549      {
550        if (debug)
551          {
552            System.out.println("\t" + filename);
553          }
554        in = new FileReader(filename);
555        parse(index, in);
556      }
557    catch (IOException e)
558      {
559        if (debug)
560          {
561            System.out.println(e.getClass().getName() + ": " +
562                               e.getMessage());
563          }
564      }
565    finally
566      {
567        if (in != null)
568          {
569            try
570              {
571                in.close();
572              }
573            catch (IOException e)
574              {
575              }
576          }
577      }
578  }
579
580  private void parseResource(int index, String name)
581  {
582    Reader in = null;
583    try
584      {
585        InputStream is = getClass().getResourceAsStream(name);
586        if (is != null)
587          {
588            if (debug)
589              {
590                System.out.println("\t" + name);
591              }
592            in = new InputStreamReader(is);
593            parse(index, in);
594          }
595      }
596    catch (IOException e)
597      {
598        if (debug)
599          {
600            System.out.println(e.getClass().getName() + ": " +
601                               e.getMessage());
602          }
603      }
604    finally
605      {
606        if (in != null)
607          {
608            try
609              {
610                in.close();
611              }
612            catch (IOException e)
613              {
614              }
615          }
616      }
617  }
618
619  private void parse(int index, Reader in)
620    throws IOException
621  {
622    BufferedReader br = new BufferedReader(in);
623    CPStringBuilder buf = null;
624    for (String line = br.readLine(); line != null; line = br.readLine())
625      {
626        line = line.trim();
627        int len = line.length();
628        if (len == 0 || line.charAt(0) == '#')
629          {
630            continue; // Comment
631          }
632        if (line.charAt(len - 1) == '\\')
633          {
634            if (buf == null)
635              {
636                buf = new CPStringBuilder();
637              }
638            buf.append(line.substring(0, len - 1));
639          }
640        else if (buf != null)
641          {
642            buf.append(line);
643            parseEntry(index, buf.toString());
644            buf = null;
645          }
646        else
647          {
648            parseEntry(index, line);
649          }
650      }
651  }
652
653  private void parseEntry(int index, String line)
654  {
655    // Tokenize entry into fields
656    char[] chars = line.toCharArray();
657    int len = chars.length;
658    boolean inQuotedString = false;
659    boolean fallback = false;
660    CPStringBuilder buffer = new CPStringBuilder();
661    List<String> fields = new ArrayList<String>();
662    for (int i = 0; i < len; i++)
663      {
664        char c = chars[i];
665        if (c == '\\')
666          {
667            c = chars[++i]; // qchar
668          }
669        if (c == ';' && !inQuotedString)
670          {
671            String field = buffer.toString().trim();
672            if ("x-java-fallback-entry".equals(field))
673              {
674                fallback = true;
675              }
676            fields.add(field);
677            buffer.setLength(0);
678          }
679        else
680          {
681            if (c == '"')
682              {
683                inQuotedString = !inQuotedString;
684              }
685            buffer.append(c);
686          }
687      }
688    String field = buffer.toString().trim();
689    if ("x-java-fallback-entry".equals(field))
690      {
691        fallback = true;
692      }
693    fields.add(field);
694
695    len = fields.size();
696    if (len < 2)
697      {
698        if (debug)
699          {
700            System.err.println("Invalid mailcap entry: " + line);
701          }
702        return;
703      }
704
705    Map<String,Map<String,List<String>>> mailcap =
706      fallback ? mailcaps[index][FALLBACK] : mailcaps[index][NORMAL];
707    String mimeType = fields.get(0);
708    addField(mailcap, mimeType, "view-command", (String) fields.get(1));
709    for (int i = 2; i < len; i++)
710      {
711        addField(mailcap, mimeType, null, (String) fields.get(i));
712      }
713  }
714
715  private void addField(Map<String,Map<String,List<String>>> mailcap,
716                        String mimeType, String verb, String command)
717  {
718    if (verb == null)
719      {
720        int ei = command.indexOf('=');
721        if (ei != -1)
722          {
723            verb = command.substring(0, ei);
724            command = command.substring(ei + 1);
725          }
726      }
727    if (command.length() == 0 || verb == null || verb.length() == 0)
728      {
729        return; // Invalid field or flag
730      }
731
732    Map<String,List<String>> commands = mailcap.get(mimeType);
733    if (commands == null)
734      {
735        commands = new LinkedHashMap<String,List<String>>();
736        mailcap.put(mimeType, commands);
737      }
738    List<String> classNames = commands.get(verb);
739    if (classNames == null)
740      {
741        classNames = new ArrayList<String>();
742        commands.put(verb, classNames);
743      }
744    classNames.add(command);
745  }
746
747  private Map<String,List<String>>
748    getCommands(Map<String,Map<String,List<String>>> mailcap,
749                String mimeType)
750  {
751    int si = mimeType.indexOf('/');
752    String genericMimeType = new CPStringBuilder(mimeType.substring(0, si))
753      .append('/')
754      .append('*')
755      .toString();
756    Map<String,List<String>> specific = mailcap.get(mimeType);
757    Map<String,List<String>> generic = mailcap.get(genericMimeType);
758    if (generic == null)
759      {
760        return specific;
761      }
762    if (specific == null)
763      {
764        return generic;
765      }
766    Map<String,List<String>> combined = new LinkedHashMap<String,List<String>>();
767    combined.putAll(specific);
768    for (String verb : generic.keySet())
769      {
770        List<String> genericClassNames = generic.get(verb);
771        List<String> classNames = combined.get(verb);
772        if (classNames == null)
773          {
774            combined.put(verb, genericClassNames);
775          }
776        else
777          {
778            classNames.addAll(genericClassNames);
779          }
780      }
781    return combined;
782  }
783
784  // -- Utility methods --
785
786  private List<URL> getSystemResources(String name)
787  {
788    List<URL> acc = new ArrayList<URL>();
789    try
790      {
791        for (Enumeration<URL> i = ClassLoader.getSystemResources(name);
792             i.hasMoreElements(); )
793          {
794            acc.add(i.nextElement());
795          }
796      }
797    catch (IOException e)
798      {
799      }
800    return acc;
801  }
802
803}