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