001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package javax.activation;
021    
022    import java.io.BufferedReader;
023    import java.io.File;
024    import java.io.FileInputStream;
025    import java.io.FileReader;
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.io.InputStreamReader;
029    import java.io.Reader;
030    import java.net.URL;
031    import java.security.Security;
032    import java.util.ArrayList;
033    import java.util.Collections;
034    import java.util.Enumeration;
035    import java.util.HashMap;
036    import java.util.Iterator;
037    import java.util.List;
038    import java.util.Map;
039    
040    /**
041     * @version $Rev: 752899 $ $Date: 2009-03-12 16:33:19 +0100 (Do, 12. M??r 2009) $
042     */
043    public class MailcapCommandMap extends CommandMap {
044        private final Map mimeTypes = new HashMap();
045        private final Map preferredCommands = new HashMap();
046        private final Map allCommands = new HashMap();
047        // the unparsed commands from the mailcap file.
048        private final Map nativeCommands = new HashMap();
049        // commands identified as fallbacks...these are used last, and also used as wildcards.
050        private final Map fallbackCommands = new HashMap();
051        private URL url;
052    
053        public MailcapCommandMap() {
054            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
055            // process /META-INF/mailcap.default
056            try {
057                InputStream is = MailcapCommandMap.class.getResourceAsStream("/META-INF/mailcap.default");
058                if (is != null) {
059                    try {
060                        parseMailcap(is);
061                    } finally {
062                        is.close();
063                    }
064                }
065            } catch (IOException e) {
066                // ignore
067            }
068    
069            // process /META-INF/mailcap resources
070            try {
071                Enumeration e = contextLoader.getResources("META-INF/mailcap");
072                while (e.hasMoreElements()) {
073                    url = ((URL) e.nextElement());
074                    try {
075                        InputStream is = url.openStream();
076                        try {
077                            parseMailcap(is);
078                        } finally {
079                            is.close();
080                        }
081                    } catch (IOException e1) {
082                        continue;
083                    }
084                }
085            } catch (SecurityException e) {
086                // ignore
087            } catch (IOException e) {
088                // ignore
089            }
090    
091            // process ${java.home}/lib/mailcap
092            try {
093                File file = new File(System.getProperty("java.home"), "lib/mailcap");
094                InputStream is = new FileInputStream(file);
095                try {
096                    parseMailcap(is);
097                } finally {
098                    is.close();
099                }
100            } catch (SecurityException e) {
101                // ignore
102            } catch (IOException e) {
103                // ignore
104            }
105    
106            // process ${user.home}/lib/mailcap
107            try {
108                File file = new File(System.getProperty("user.home"), ".mailcap");
109                InputStream is = new FileInputStream(file);
110                try {
111                    parseMailcap(is);
112                } finally {
113                    is.close();
114                }
115            } catch (SecurityException e) {
116                // ignore
117            } catch (IOException e) {
118                // ignore
119            }
120        }
121    
122        public MailcapCommandMap(String fileName) throws IOException {
123            this();
124            FileReader reader = new FileReader(fileName);
125            try {
126                parseMailcap(reader);
127            } finally {
128                reader.close();
129            }
130        }
131    
132        public MailcapCommandMap(InputStream is) {
133            this();
134            parseMailcap(is);
135        }
136    
137        private void parseMailcap(InputStream is) {
138            try {
139                parseMailcap(new InputStreamReader(is));
140            } catch (IOException e) {
141                // spec API means all we can do is swallow this
142            }
143        }
144    
145        void parseMailcap(Reader reader) throws IOException {
146            BufferedReader br = new BufferedReader(reader);
147            String line;
148            while ((line = br.readLine()) != null) {
149                addMailcap(line);
150            }
151        }
152    
153        public synchronized void addMailcap(String mail_cap) {
154            int index = 0;
155            // skip leading whitespace
156            index = skipSpace(mail_cap, index);
157            if (index == mail_cap.length() || mail_cap.charAt(index) == '#') {
158                return;
159            }
160    
161            // get primary type
162            int start = index;
163            index = getToken(mail_cap, index);
164            if (start == index) {
165                return;
166            }
167            String mimeType = mail_cap.substring(start, index);
168    
169            // skip any spaces after the primary type
170            index = skipSpace(mail_cap, index);
171            if (index == mail_cap.length() || mail_cap.charAt(index) == '#') {
172                return;
173            }
174    
175            // get sub-type
176            if (mail_cap.charAt(index) == '/') {
177                index = skipSpace(mail_cap, ++index);
178                start = index;
179                index = getToken(mail_cap, index);
180                mimeType = mimeType + '/' + mail_cap.substring(start, index);
181            } else {
182    
183                mimeType = mimeType + "/*";
184            }
185    
186            // we record all mappings using the lowercase version.
187            mimeType = mimeType.toLowerCase();
188    
189            // skip spaces after mime type
190            index = skipSpace(mail_cap, index);
191    
192            // expect a ';' to terminate field 1
193            if (index == mail_cap.length() || mail_cap.charAt(index) != ';') {
194                return;
195            }
196            // ok, we've parsed the mime text field, now parse the view field.  If there's something
197            // there, then we add this to the native text.
198            index = skipSpace(mail_cap, index + 1);
199            // if the next encountered text is not a ";", then we have a view.  This gets added to the
200            // native list.
201            if (index == mail_cap.length() || mail_cap.charAt(index) != ';') {
202                ArrayList nativeCommandList = (ArrayList)nativeCommands.get(mimeType);
203    
204                // if this is the first for this mimetype, create a holder
205                if (nativeCommandList == null) {
206                    nativeCommandList = new ArrayList();
207                    nativeCommands.put(mimeType, nativeCommandList);
208                }
209    
210                // now add this as an entry in the list.
211                nativeCommandList.add(mail_cap);
212                // now skip forward to the next field marker, if any
213                index = getMText(mail_cap, index);
214            }
215    
216            // we don't know which list this will be added to until we finish parsing, as there
217            // can be an x-java-fallback-entry parameter that moves this to the fallback list.
218            List commandList = new ArrayList();
219            // but by default, this is not a fallback.
220            boolean fallback = false;
221    
222            int fieldNumber = 0;
223    
224            // parse fields
225            while (index < mail_cap.length() && mail_cap.charAt(index) == ';') {
226                index = skipSpace(mail_cap, index + 1);
227                start = index;
228                index = getToken(mail_cap, index);
229                String fieldName = mail_cap.substring(start, index).toLowerCase();
230                index = skipSpace(mail_cap, index);
231                if (index < mail_cap.length() && mail_cap.charAt(index) == '=') {
232                    index = skipSpace(mail_cap, index + 1);
233                    start = index;
234                    index = getMText(mail_cap, index);
235                    String value = mail_cap.substring(start, index);
236                    index = skipSpace(mail_cap, index);
237                    if (fieldName.startsWith("x-java-") && fieldName.length() > 7) {
238                        String command = fieldName.substring(7);
239                        value = value.trim();
240                        if (command.equals("fallback-entry")) {
241                            if (value.equals("true")) {
242                                fallback = true;
243                            }
244                        }
245                        else {
246                            // create a CommandInfo item and add it the accumulator
247                            CommandInfo info = new CommandInfo(command, value);
248                            commandList.add(info);
249                        }
250                    }
251                }
252            }
253            addCommands(mimeType, commandList, fallback);
254        }
255    
256        /**
257         * Add a parsed list of commands to the appropriate command list.
258         *
259         * @param mimeType The mimeType name this is added under.
260         * @param commands A List containing the command information.
261         * @param fallback The target list identifier.
262         */
263        protected void addCommands(String mimeType, List commands, boolean fallback) {
264            // add this to the mimeType set
265            mimeTypes.put(mimeType, mimeType);
266            // the target list changes based on the type of entry.
267            Map target = fallback ? fallbackCommands : preferredCommands;
268    
269            // now process
270            for (Iterator i = commands.iterator(); i.hasNext();) {
271                CommandInfo info = (CommandInfo)i.next();
272                addCommand(target, mimeType, info);
273                // if this is not a fallback position, then this to the allcommands list.
274                if (!fallback) {
275                    List cmdList = (List) allCommands.get(mimeType);
276                    if (cmdList == null) {
277                        cmdList = new ArrayList();
278                        allCommands.put(mimeType, cmdList);
279                    }
280                    cmdList.add(info);
281                }
282            }
283        }
284    
285    
286        /**
287         * Add a command to a target command list (preferred or fallback).
288         *
289         * @param commandList
290         *                 The target command list.
291         * @param mimeType The MIME type the command is associated with.
292         * @param command  The command information.
293         */
294        protected void addCommand(Map commandList, String mimeType, CommandInfo command) {
295    
296            Map commands = (Map) commandList.get(mimeType);
297            if (commands == null) {
298                commands = new HashMap();
299                commandList.put(mimeType, commands);
300            }
301            commands.put(command.getCommandName(), command);
302        }
303    
304    
305        private int skipSpace(String s, int index) {
306            while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
307                index++;
308            }
309            return index;
310        }
311    
312        private int getToken(String s, int index) {
313            while (index < s.length() && s.charAt(index) != '#' && !MimeType.isSpecial(s.charAt(index))) {
314                index++;
315            }
316            return index;
317        }
318    
319        private int getMText(String s, int index) {
320            while (index < s.length()) {
321                char c = s.charAt(index);
322                if (c == '#' || c == ';' || Character.isISOControl(c)) {
323                    return index;
324                }
325                if (c == '\\') {
326                    index++;
327                    if (index == s.length()) {
328                        return index;
329                    }
330                }
331                index++;
332            }
333            return index;
334        }
335    
336        public synchronized CommandInfo[] getPreferredCommands(String mimeType) {
337            // get the mimetype as a lowercase version.
338            mimeType = mimeType.toLowerCase();
339    
340            Map commands = (Map) preferredCommands.get(mimeType);
341            if (commands == null) {
342                commands = (Map) preferredCommands.get(getWildcardMimeType(mimeType));
343            }
344    
345            Map fallbackCommands = getFallbackCommands(mimeType);
346    
347            // if we have fall backs, then we need to merge this stuff.
348            if (fallbackCommands != null) {
349                // if there's no command list, we can just use this as the master list.
350                if (commands == null) {
351                    commands = fallbackCommands;
352                }
353                else {
354                    // merge the two lists.  The ones in the commands list will take precedence.
355                    commands = mergeCommandMaps(commands, fallbackCommands);
356                }
357            }
358    
359            // now convert this into an array result.
360            if (commands == null) {
361                return new CommandInfo[0];
362            }
363            return (CommandInfo[]) commands.values().toArray(new CommandInfo[commands.size()]);
364        }
365    
366        private Map getFallbackCommands(String mimeType) {
367            Map commands = (Map) fallbackCommands.get(mimeType);
368    
369            // now we also need to search this as if it was a wildcard.  If we get a wildcard hit,
370            // we have to merge the two lists.
371            Map wildcardCommands = (Map)fallbackCommands.get(getWildcardMimeType(mimeType));
372            // no wildcard version
373            if (wildcardCommands == null) {
374                return commands;
375            }
376            // we need to merge these.
377            return mergeCommandMaps(commands, wildcardCommands);
378        }
379    
380    
381        private Map mergeCommandMaps(Map main, Map fallback) {
382            // create a cloned copy of the second map.  We're going to use a PutAll operation to
383            // overwrite any duplicates.
384            Map result = new HashMap(fallback);
385            result.putAll(main);
386    
387            return result;
388        }
389    
390        public synchronized CommandInfo[] getAllCommands(String mimeType) {
391            mimeType = mimeType.toLowerCase();
392            List exactCommands = (List) allCommands.get(mimeType);
393            if (exactCommands == null) {
394                exactCommands = Collections.EMPTY_LIST;
395            }
396            List wildCommands = (List) allCommands.get(getWildcardMimeType(mimeType));
397            if (wildCommands == null) {
398                wildCommands = Collections.EMPTY_LIST;
399            }
400    
401            Map fallbackCommands = getFallbackCommands(mimeType);
402            if (fallbackCommands == null) {
403                fallbackCommands = Collections.EMPTY_MAP;
404            }
405    
406    
407            CommandInfo[] result = new CommandInfo[exactCommands.size() + wildCommands.size() + fallbackCommands.size()];
408            int j = 0;
409            for (int i = 0; i < exactCommands.size(); i++) {
410                result[j++] = (CommandInfo) exactCommands.get(i);
411            }
412            for (int i = 0; i < wildCommands.size(); i++) {
413                result[j++] = (CommandInfo) wildCommands.get(i);
414            }
415    
416            for (Iterator i = fallbackCommands.keySet().iterator(); i.hasNext();) {
417                result[j++] = (CommandInfo) fallbackCommands.get((String)i.next());
418            }
419            return result;
420        }
421    
422        public synchronized CommandInfo getCommand(String mimeType, String cmdName) {
423            mimeType = mimeType.toLowerCase();
424            // strip any parameters from the supplied mimeType
425            int i = mimeType.indexOf(';');
426            if (i != -1) {
427                mimeType = mimeType.substring(0, i).trim();
428            }
429    
430            // search for an exact match
431            Map commands = (Map) preferredCommands.get(mimeType);
432            if (commands == null) {
433                // then a wild card match
434                commands = (Map) preferredCommands.get(getWildcardMimeType(mimeType));
435                if (commands == null) {
436                    // then fallback searches, both standard and wild card.
437                    commands = (Map) fallbackCommands.get(mimeType);
438                    if (commands == null) {
439                        commands = (Map) fallbackCommands.get(getWildcardMimeType(mimeType));
440                    }
441                    if (commands == null) {
442                        return null;
443                    }
444                }
445            }
446            return (CommandInfo) commands.get(cmdName.toLowerCase());
447        }
448    
449        private String getWildcardMimeType(String mimeType) {
450            int i = mimeType.indexOf('/');
451            if (i == -1) {
452                return mimeType + "/*";
453            } else {
454                return mimeType.substring(0, i + 1) + "*";
455            }
456        }
457    
458        public synchronized DataContentHandler createDataContentHandler(String mimeType) {
459    
460            CommandInfo info = getCommand(mimeType, "content-handler");
461            if (info == null) {
462                return null;
463            }
464    
465            ClassLoader cl = Thread.currentThread().getContextClassLoader();
466            if (cl == null) {
467                cl = getClass().getClassLoader();
468            }
469            try {
470                return (DataContentHandler) cl.loadClass(info.getCommandClass()).newInstance();
471            } catch (ClassNotFoundException e) {
472                return null;
473            } catch (IllegalAccessException e) {
474                return null;
475            } catch (InstantiationException e) {
476                return null;
477            }
478        }
479    
480        /**
481         * Get all MIME types known to this command map.
482         *
483         * @return A String array of the MIME type names.
484         */
485        public synchronized String[] getMimeTypes() {
486            ArrayList types = new ArrayList(mimeTypes.values());
487            return (String[])types.toArray(new String[types.size()]);
488        }
489    
490        /**
491         * Return the list of raw command strings parsed
492         * from the mailcap files for a given mimeType.
493         *
494         * @param mimeType The target mime type
495         *
496         * @return A String array of the raw command strings.  Returns
497         *         an empty array if the mimetype is not currently known.
498         */
499        public synchronized String[] getNativeCommands(String mimeType) {
500            ArrayList commands = (ArrayList)nativeCommands.get(mimeType.toLowerCase());
501            if (commands == null) {
502                return new String[0];
503            }
504            return (String[])commands.toArray(new String[commands.size()]);
505        }
506    }