001    /* MailcapCommandMap.java -- Command map implementation using a mailcap file.
002       Copyright (C) 2004 Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    package javax.activation;
039    
040    import gnu.java.lang.CPStringBuilder;
041    
042    import java.io.BufferedReader;
043    import java.io.File;
044    import java.io.FileReader;
045    import java.io.InputStream;
046    import java.io.InputStreamReader;
047    import java.io.IOException;
048    import java.io.Reader;
049    import java.io.StringReader;
050    import java.net.URL;
051    import java.util.ArrayList;
052    import java.util.Enumeration;
053    import java.util.LinkedHashMap;
054    import java.util.Iterator;
055    import java.util.List;
056    import 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     */
073    public 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    }
804