1 /*
   2  * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.activation.registries;
  27 
  28 import java.io.*;
  29 import java.util.*;
  30 
  31 public class MailcapFile {
  32 
  33     /**
  34      * A Map indexed by MIME type (string) that references
  35      * a Map of commands for each type.  The comand Map
  36      * is indexed by the command name and references a List of
  37      * class names (strings) for each command.
  38      */
  39     private Map type_hash = new HashMap();
  40 
  41     /**
  42      * Another Map like above, but for fallback entries.
  43      */
  44     private Map fallback_hash = new HashMap();
  45 
  46     /**
  47      * A Map indexed by MIME type (string) that references
  48      * a List of native commands (string) corresponding to the type.
  49      */
  50     private Map native_commands = new HashMap();
  51 
  52     private static boolean addReverse = false;
  53 
  54     static {
  55         try {
  56             addReverse = Boolean.getBoolean("javax.activation.addreverse");
  57         } catch (Throwable t) {
  58             // ignore any errors
  59         }
  60     }
  61 
  62     /**
  63      * The constructor that takes a filename as an argument.
  64      *
  65      * @param new_fname The file name of the mailcap file.
  66      */
  67     public MailcapFile(String new_fname) throws IOException {
  68         if (LogSupport.isLoggable())
  69             LogSupport.log("new MailcapFile: file " + new_fname);
  70         FileReader reader = null;
  71         try {
  72             reader = new FileReader(new_fname);
  73             parse(new BufferedReader(reader));
  74         } finally {
  75             if (reader != null) {
  76                 try {
  77                     reader.close();
  78                 } catch (IOException ex) { }
  79             }
  80         }
  81     }
  82 
  83     /**
  84      * The constructor that takes an input stream as an argument.
  85      *
  86      * @param is        the input stream
  87      */
  88     public MailcapFile(InputStream is) throws IOException {
  89         if (LogSupport.isLoggable())
  90             LogSupport.log("new MailcapFile: InputStream");
  91         parse(new BufferedReader(new InputStreamReader(is, "iso-8859-1")));
  92     }
  93 
  94     /**
  95      * Mailcap file default constructor.
  96      */
  97     public MailcapFile() {
  98         if (LogSupport.isLoggable())
  99             LogSupport.log("new MailcapFile: default");
 100     }
 101 
 102     /**
 103      * Get the Map of MailcapEntries based on the MIME type.
 104      *
 105      * <p>
 106      * <strong>Semantics:</strong> First check for the literal mime type,
 107      * if that fails looks for wildcard <type>/\* and return that. Return the
 108      * list of all that hit.
 109      */
 110     public Map getMailcapList(String mime_type) {
 111         Map search_result = null;
 112         Map wildcard_result = null;
 113 
 114         // first try the literal
 115         search_result = (Map)type_hash.get(mime_type);
 116 
 117         // ok, now try the wildcard
 118         int separator = mime_type.indexOf('/');
 119         String subtype = mime_type.substring(separator + 1);
 120         if (!subtype.equals("*")) {
 121             String type = mime_type.substring(0, separator + 1) + "*";
 122             wildcard_result = (Map)type_hash.get(type);
 123 
 124             if (wildcard_result != null) { // damn, we have to merge!!!
 125                 if (search_result != null)
 126                     search_result =
 127                         mergeResults(search_result, wildcard_result);
 128                 else
 129                     search_result = wildcard_result;
 130             }
 131         }
 132         return search_result;
 133     }
 134 
 135     /**
 136      * Get the Map of fallback MailcapEntries based on the MIME type.
 137      *
 138      * <p>
 139      * <strong>Semantics:</strong> First check for the literal mime type,
 140      * if that fails looks for wildcard <type>/\* and return that. Return the
 141      * list of all that hit.
 142      */
 143     public Map getMailcapFallbackList(String mime_type) {
 144         Map search_result = null;
 145         Map wildcard_result = null;
 146 
 147         // first try the literal
 148         search_result = (Map)fallback_hash.get(mime_type);
 149 
 150         // ok, now try the wildcard
 151         int separator = mime_type.indexOf('/');
 152         String subtype = mime_type.substring(separator + 1);
 153         if (!subtype.equals("*")) {
 154             String type = mime_type.substring(0, separator + 1) + "*";
 155             wildcard_result = (Map)fallback_hash.get(type);
 156 
 157             if (wildcard_result != null) { // damn, we have to merge!!!
 158                 if (search_result != null)
 159                     search_result =
 160                         mergeResults(search_result, wildcard_result);
 161                 else
 162                     search_result = wildcard_result;
 163             }
 164         }
 165         return search_result;
 166     }
 167 
 168     /**
 169      * Return all the MIME types known to this mailcap file.
 170      */
 171     public String[] getMimeTypes() {
 172         Set types = new HashSet(type_hash.keySet());
 173         types.addAll(fallback_hash.keySet());
 174         types.addAll(native_commands.keySet());
 175         String[] mts = new String[types.size()];
 176         mts = (String[])types.toArray(mts);
 177         return mts;
 178     }
 179 
 180     /**
 181      * Return all the native comands for the given MIME type.
 182      */
 183     public String[] getNativeCommands(String mime_type) {
 184         String[] cmds = null;
 185         List v =
 186             (List)native_commands.get(mime_type.toLowerCase(Locale.ENGLISH));
 187         if (v != null) {
 188             cmds = new String[v.size()];
 189             cmds = (String[])v.toArray(cmds);
 190         }
 191         return cmds;
 192     }
 193 
 194     /**
 195      * Merge the first hash into the second.
 196      * This merge will only effect the hashtable that is
 197      * returned, we don't want to touch the one passed in since
 198      * its integrity must be maintained.
 199      */
 200     private Map mergeResults(Map first, Map second) {
 201         Iterator verb_enum = second.keySet().iterator();
 202         Map clonedHash = new HashMap(first);
 203 
 204         // iterate through the verbs in the second map
 205         while (verb_enum.hasNext()) {
 206             String verb = (String)verb_enum.next();
 207             List cmdVector = (List)clonedHash.get(verb);
 208             if (cmdVector == null) {
 209                 clonedHash.put(verb, second.get(verb));
 210             } else {
 211                 // merge the two
 212                 List oldV = (List)second.get(verb);
 213                 cmdVector = new ArrayList(cmdVector);
 214                 cmdVector.addAll(oldV);
 215                 clonedHash.put(verb, cmdVector);
 216             }
 217         }
 218         return clonedHash;
 219     }
 220 
 221     /**
 222      * appendToMailcap: Append to this Mailcap DB, use the mailcap
 223      * format:
 224      * Comment == "# <i>comment string</i>
 225      * Entry == "mimetype;        javabeanclass<nl>
 226      *
 227      * Example:
 228      * # this is a comment
 229      * image/gif       jaf.viewers.ImageViewer
 230      */
 231     public void appendToMailcap(String mail_cap) {
 232         if (LogSupport.isLoggable())
 233             LogSupport.log("appendToMailcap: " + mail_cap);
 234         try {
 235             parse(new StringReader(mail_cap));
 236         } catch (IOException ex) {
 237             // can't happen
 238         }
 239     }
 240 
 241     /**
 242      * parse file into a hash table of MC Type Entry Obj
 243      */
 244     private void parse(Reader reader) throws IOException {
 245         BufferedReader buf_reader = new BufferedReader(reader);
 246         String line = null;
 247         String continued = null;
 248 
 249         while ((line = buf_reader.readLine()) != null) {
 250             //    LogSupport.log("parsing line: " + line);
 251 
 252             line = line.trim();
 253 
 254             try {
 255                 if (line.charAt(0) == '#')
 256                     continue;
 257                 if (line.charAt(line.length() - 1) == '\\') {
 258                     if (continued != null)
 259                         continued += line.substring(0, line.length() - 1);
 260                     else
 261                         continued = line.substring(0, line.length() - 1);
 262                 } else if (continued != null) {
 263                     // handle the two strings
 264                     continued = continued + line;
 265                     //  LogSupport.log("parse: " + continued);
 266                     try {
 267                         parseLine(continued);
 268                     } catch (MailcapParseException e) {
 269                         //e.printStackTrace();
 270                     }
 271                     continued = null;
 272                 }
 273                 else {
 274                     //  LogSupport.log("parse: " + line);
 275                     try {
 276                         parseLine(line);
 277                         // LogSupport.log("hash.size = " + type_hash.size());
 278                     } catch (MailcapParseException e) {
 279                         //e.printStackTrace();
 280                     }
 281                 }
 282             } catch (StringIndexOutOfBoundsException e) {}
 283         }
 284     }
 285 
 286     /**
 287      *  A routine to parse individual entries in a Mailcap file.
 288      *
 289      *  Note that this routine does not handle line continuations.
 290      *  They should have been handled prior to calling this routine.
 291      */
 292     protected void parseLine(String mailcapEntry)
 293                                 throws MailcapParseException, IOException {
 294         MailcapTokenizer tokenizer = new MailcapTokenizer(mailcapEntry);
 295         tokenizer.setIsAutoquoting(false);
 296 
 297         if (LogSupport.isLoggable())
 298             LogSupport.log("parse: " + mailcapEntry);
 299         //      parse the primary type
 300         int currentToken = tokenizer.nextToken();
 301         if (currentToken != MailcapTokenizer.STRING_TOKEN) {
 302             reportParseError(MailcapTokenizer.STRING_TOKEN, currentToken,
 303                                         tokenizer.getCurrentTokenValue());
 304         }
 305         String primaryType =
 306             tokenizer.getCurrentTokenValue().toLowerCase(Locale.ENGLISH);
 307         String subType = "*";
 308 
 309         //      parse the '/' between primary and sub
 310         //      if it's not present that's ok, we just don't have a subtype
 311         currentToken = tokenizer.nextToken();
 312         if ((currentToken != MailcapTokenizer.SLASH_TOKEN) &&
 313                         (currentToken != MailcapTokenizer.SEMICOLON_TOKEN)) {
 314             reportParseError(MailcapTokenizer.SLASH_TOKEN,
 315                                 MailcapTokenizer.SEMICOLON_TOKEN, currentToken,
 316                                 tokenizer.getCurrentTokenValue());
 317         }
 318 
 319         //      only need to look for a sub type if we got a '/'
 320         if (currentToken == MailcapTokenizer.SLASH_TOKEN) {
 321             //  parse the sub type
 322             currentToken = tokenizer.nextToken();
 323             if (currentToken != MailcapTokenizer.STRING_TOKEN) {
 324                 reportParseError(MailcapTokenizer.STRING_TOKEN,
 325                             currentToken, tokenizer.getCurrentTokenValue());
 326             }
 327             subType =
 328                 tokenizer.getCurrentTokenValue().toLowerCase(Locale.ENGLISH);
 329 
 330             //  get the next token to simplify the next step
 331             currentToken = tokenizer.nextToken();
 332         }
 333 
 334         String mimeType = primaryType + "/" + subType;
 335 
 336         if (LogSupport.isLoggable())
 337             LogSupport.log("  Type: " + mimeType);
 338 
 339         //      now setup the commands hashtable
 340         Map commands = new LinkedHashMap();     // keep commands in order found
 341 
 342         //      parse the ';' that separates the type from the parameters
 343         if (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) {
 344             reportParseError(MailcapTokenizer.SEMICOLON_TOKEN,
 345                             currentToken, tokenizer.getCurrentTokenValue());
 346         }
 347         //      eat it
 348 
 349         //      parse the required view command
 350         tokenizer.setIsAutoquoting(true);
 351         currentToken = tokenizer.nextToken();
 352         tokenizer.setIsAutoquoting(false);
 353         if ((currentToken != MailcapTokenizer.STRING_TOKEN) &&
 354                     (currentToken != MailcapTokenizer.SEMICOLON_TOKEN)) {
 355             reportParseError(MailcapTokenizer.STRING_TOKEN,
 356                             MailcapTokenizer.SEMICOLON_TOKEN, currentToken,
 357                             tokenizer.getCurrentTokenValue());
 358         }
 359 
 360         if (currentToken == MailcapTokenizer.STRING_TOKEN) {
 361             // have a native comand, save the entire mailcap entry
 362             //String nativeCommand = tokenizer.getCurrentTokenValue();
 363             List v = (List)native_commands.get(mimeType);
 364             if (v == null) {
 365                 v = new ArrayList();
 366                 v.add(mailcapEntry);
 367                 native_commands.put(mimeType, v);
 368             } else {
 369                 // XXX - check for duplicates?
 370                 v.add(mailcapEntry);
 371             }
 372         }
 373 
 374         //      only have to get the next token if the current one isn't a ';'
 375         if (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) {
 376             currentToken = tokenizer.nextToken();
 377         }
 378 
 379         // look for a ';' which will indicate whether
 380         // a parameter list is present or not
 381         if (currentToken == MailcapTokenizer.SEMICOLON_TOKEN) {
 382             boolean isFallback = false;
 383             do {
 384                 //      eat the ';'
 385 
 386                 //      parse the parameter name
 387                 currentToken = tokenizer.nextToken();
 388                 if (currentToken != MailcapTokenizer.STRING_TOKEN) {
 389                     reportParseError(MailcapTokenizer.STRING_TOKEN,
 390                             currentToken, tokenizer.getCurrentTokenValue());
 391                 }
 392                 String paramName = tokenizer.getCurrentTokenValue().
 393                                                 toLowerCase(Locale.ENGLISH);
 394 
 395                 //      parse the '=' which separates the name from the value
 396                 currentToken = tokenizer.nextToken();
 397                 if ((currentToken != MailcapTokenizer.EQUALS_TOKEN) &&
 398                     (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) &&
 399                     (currentToken != MailcapTokenizer.EOI_TOKEN)) {
 400                     reportParseError(MailcapTokenizer.EQUALS_TOKEN,
 401                             MailcapTokenizer.SEMICOLON_TOKEN,
 402                             MailcapTokenizer.EOI_TOKEN,
 403                             currentToken, tokenizer.getCurrentTokenValue());
 404                 }
 405 
 406                 //      we only have a useful command if it is named
 407                 if (currentToken == MailcapTokenizer.EQUALS_TOKEN) {
 408                     //  eat it
 409 
 410                     //  parse the parameter value (which is autoquoted)
 411                     tokenizer.setIsAutoquoting(true);
 412                     currentToken = tokenizer.nextToken();
 413                     tokenizer.setIsAutoquoting(false);
 414                     if (currentToken != MailcapTokenizer.STRING_TOKEN) {
 415                         reportParseError(MailcapTokenizer.STRING_TOKEN,
 416                         currentToken, tokenizer.getCurrentTokenValue());
 417                     }
 418                     String paramValue =
 419                                 tokenizer.getCurrentTokenValue();
 420 
 421                     // add the class to the list iff it is one we care about
 422                     if (paramName.startsWith("x-java-")) {
 423                         String commandName = paramName.substring(7);
 424                         //      7 == "x-java-".length
 425 
 426                         if (commandName.equals("fallback-entry") &&
 427                             paramValue.equalsIgnoreCase("true")) {
 428                             isFallback = true;
 429                         } else {
 430 
 431                             //  setup the class entry list
 432                             if (LogSupport.isLoggable())
 433                                 LogSupport.log("    Command: " + commandName +
 434                                                     ", Class: " + paramValue);
 435                             List classes = (List)commands.get(commandName);
 436                             if (classes == null) {
 437                                 classes = new ArrayList();
 438                                 commands.put(commandName, classes);
 439                             }
 440                             if (addReverse)
 441                                 classes.add(0, paramValue);
 442                             else
 443                                 classes.add(paramValue);
 444                         }
 445                     }
 446 
 447                     //  set up the next iteration
 448                     currentToken = tokenizer.nextToken();
 449                 }
 450             } while (currentToken == MailcapTokenizer.SEMICOLON_TOKEN);
 451 
 452             Map masterHash = isFallback ? fallback_hash : type_hash;
 453             Map curcommands =
 454                 (Map)masterHash.get(mimeType);
 455             if (curcommands == null) {
 456                 masterHash.put(mimeType, commands);
 457             } else {
 458                 if (LogSupport.isLoggable())
 459                     LogSupport.log("Merging commands for type " + mimeType);
 460                 // have to merge current and new commands
 461                 // first, merge list of classes for commands already known
 462                 Iterator cn = curcommands.keySet().iterator();
 463                 while (cn.hasNext()) {
 464                     String cmdName = (String)cn.next();
 465                     List ccv = (List)curcommands.get(cmdName);
 466                     List cv = (List)commands.get(cmdName);
 467                     if (cv == null)
 468                         continue;
 469                     // add everything in cv to ccv, if it's not already there
 470                     Iterator cvn = cv.iterator();
 471                     while (cvn.hasNext()) {
 472                         String clazz = (String)cvn.next();
 473                         if (!ccv.contains(clazz))
 474                             if (addReverse)
 475                                 ccv.add(0, clazz);
 476                             else
 477                                 ccv.add(clazz);
 478                     }
 479                 }
 480                 // now, add commands not previously known
 481                 cn = commands.keySet().iterator();
 482                 while (cn.hasNext()) {
 483                     String cmdName = (String)cn.next();
 484                     if (curcommands.containsKey(cmdName))
 485                         continue;
 486                     List cv = (List)commands.get(cmdName);
 487                     curcommands.put(cmdName, cv);
 488                 }
 489             }
 490         } else if (currentToken != MailcapTokenizer.EOI_TOKEN) {
 491             reportParseError(MailcapTokenizer.EOI_TOKEN,
 492                 MailcapTokenizer.SEMICOLON_TOKEN,
 493                 currentToken, tokenizer.getCurrentTokenValue());
 494         }
 495      }
 496 
 497      protected static void reportParseError(int expectedToken, int actualToken,
 498                 String actualTokenValue) throws MailcapParseException {
 499         throw new MailcapParseException("Encountered a " +
 500                 MailcapTokenizer.nameForToken(actualToken) + " token (" +
 501                 actualTokenValue + ") while expecting a " +
 502                 MailcapTokenizer.nameForToken(expectedToken) + " token.");
 503      }
 504 
 505      protected static void reportParseError(int expectedToken,
 506         int otherExpectedToken, int actualToken, String actualTokenValue)
 507                                         throws MailcapParseException {
 508         throw new MailcapParseException("Encountered a " +
 509                 MailcapTokenizer.nameForToken(actualToken) + " token (" +
 510                 actualTokenValue + ") while expecting a " +
 511                 MailcapTokenizer.nameForToken(expectedToken) + " or a " +
 512                 MailcapTokenizer.nameForToken(otherExpectedToken) + " token.");
 513      }
 514 
 515      protected static void reportParseError(int expectedToken,
 516             int otherExpectedToken, int anotherExpectedToken, int actualToken,
 517             String actualTokenValue) throws MailcapParseException {
 518         if (LogSupport.isLoggable())
 519             LogSupport.log("PARSE ERROR: " + "Encountered a " +
 520                 MailcapTokenizer.nameForToken(actualToken) + " token (" +
 521                 actualTokenValue + ") while expecting a " +
 522                 MailcapTokenizer.nameForToken(expectedToken) + ", a " +
 523                 MailcapTokenizer.nameForToken(otherExpectedToken) + ", or a " +
 524                 MailcapTokenizer.nameForToken(anotherExpectedToken) + " token.");
 525         throw new MailcapParseException("Encountered a " +
 526                 MailcapTokenizer.nameForToken(actualToken) + " token (" +
 527                 actualTokenValue + ") while expecting a " +
 528                 MailcapTokenizer.nameForToken(expectedToken) + ", a " +
 529                 MailcapTokenizer.nameForToken(otherExpectedToken) + ", or a " +
 530                 MailcapTokenizer.nameForToken(anotherExpectedToken) + " token.");
 531      }
 532 
 533      /** for debugging
 534      public static void main(String[] args) throws Exception {
 535         Map masterHash = new HashMap();
 536         for (int i = 0; i < args.length; ++i) {
 537             System.out.println("Entry " + i + ": " + args[i]);
 538             parseLine(args[i], masterHash);
 539         }
 540 
 541         Enumeration types = masterHash.keys();
 542         while (types.hasMoreElements()) {
 543             String key = (String)types.nextElement();
 544             System.out.println("MIME Type: " + key);
 545 
 546             Map commandHash = (Map)masterHash.get(key);
 547             Enumeration commands = commandHash.keys();
 548             while (commands.hasMoreElements()) {
 549                 String command = (String)commands.nextElement();
 550                 System.out.println("    Command: " + command);
 551 
 552                 Vector classes = (Vector)commandHash.get(command);
 553                 for (int i = 0; i < classes.size(); ++i) {
 554                         System.out.println("        Class: " +
 555                                             (String)classes.elementAt(i));
 556                 }
 557             }
 558 
 559             System.out.println("");
 560         }
 561     }
 562     */
 563 }