|
68 | 68 | import org.slf4j.LoggerFactory; |
69 | 69 | import org.xml.sax.SAXException; |
70 | 70 |
|
71 | | -import org.apache.tika.detect.CharsetSupersets; |
| 71 | +import org.apache.tika.detect.DefaultEncodingDetector; |
| 72 | +import org.apache.tika.detect.EncodingDetector; |
72 | 73 | import org.apache.tika.detect.EncodingResult; |
73 | 74 | import org.apache.tika.exception.TikaException; |
74 | 75 | import org.apache.tika.extractor.EmbeddedDocumentUtil; |
|
81 | 82 | import org.apache.tika.metadata.TikaCoreProperties; |
82 | 83 | import org.apache.tika.parser.ParseContext; |
83 | 84 | import org.apache.tika.parser.Parser; |
84 | | -import org.apache.tika.parser.html.HtmlEncodingDetector; |
85 | 85 | import org.apache.tika.parser.html.JSoupParser; |
86 | 86 | import org.apache.tika.parser.mailcommons.MailDateParser; |
87 | 87 | import org.apache.tika.parser.microsoft.msg.ExtendedMetadataExtractor; |
88 | 88 | import org.apache.tika.parser.microsoft.rtf.RTFParser; |
89 | 89 | import org.apache.tika.parser.microsoft.rtf.jflex.RTFHtmlDecapsulator; |
90 | | -import org.apache.tika.parser.txt.CharsetDetector; |
91 | | -import org.apache.tika.parser.txt.CharsetMatch; |
92 | 90 | import org.apache.tika.sax.BodyContentHandler; |
93 | 91 | import org.apache.tika.sax.EmbeddedContentHandler; |
94 | 92 | import org.apache.tika.sax.XHTMLContentHandler; |
@@ -183,7 +181,7 @@ private static void loadMessageClasses() { |
183 | 181 | private final DirectoryNode root; |
184 | 182 | private final MAPIMessage msg; |
185 | 183 | private final ParseContext parseContext; |
186 | | - HtmlEncodingDetector detector = new HtmlEncodingDetector(); |
| 184 | + private static final EncodingDetector DEFAULT_ENCODING_DETECTOR = new DefaultEncodingDetector(); |
187 | 185 |
|
188 | 186 |
|
189 | 187 | public OutlookExtractor(DirectoryNode root, Metadata metadata, ParseContext context) throws TikaException { |
@@ -848,92 +846,91 @@ private void guess7BitEncoding(MAPIMessage msg) { |
848 | 846 | return; |
849 | 847 | } |
850 | 848 |
|
| 849 | + // A declared charset (message codepage, else a Content-Type header) is a hint, |
| 850 | + // not a verdict -- the detector evaluates it against the raw body bytes below. |
| 851 | + String declared = declaredCharset(msg, mainChunks); |
| 852 | + |
| 853 | + // Detect on the raw body bytes -- the HTML binary chunk if present, else the |
| 854 | + // text body. (msg.getHtmlBody() is an already-decoded String, so detecting its |
| 855 | + // re-encoded bytes would just report the re-encoding charset.) |
| 856 | + byte[] body = null; |
| 857 | + ByteChunk htmlBinary = mainChunks.getHtmlBodyChunkBinary(); |
| 858 | + if (htmlBinary != null && htmlBinary.getValue() != null) { |
| 859 | + body = htmlBinary.getValue(); |
| 860 | + } else if (mainChunks.getTextBodyChunk() != null) { |
| 861 | + body = mainChunks.getTextBodyChunk().getRawValue(); |
| 862 | + } |
| 863 | + |
| 864 | + EncodingDetector encodingDetector = context.get(EncodingDetector.class); |
| 865 | + if (encodingDetector == null) { |
| 866 | + encodingDetector = DEFAULT_ENCODING_DETECTOR; |
| 867 | + } |
| 868 | + |
| 869 | + if (body != null && body.length > 0) { |
| 870 | + Metadata metadata = new Metadata(); |
| 871 | + if (declared != null) { |
| 872 | + metadata.set(TikaCoreProperties.CONTENT_TYPE_HINT, |
| 873 | + "text/plain; charset=" + declared); |
| 874 | + } |
| 875 | + try (TikaInputStream tis = TikaInputStream.get(body)) { |
| 876 | + List<EncodingResult> results = encodingDetector.detect(tis, metadata, context); |
| 877 | + if (!results.isEmpty() && results.get(0).getConfidence() > 0.35f |
| 878 | + && tryToSet7BitEncoding(msg, results.get(0).getDecodeAs().name())) { |
| 879 | + return; |
| 880 | + } |
| 881 | + } catch (IOException e) { |
| 882 | + //swallow |
| 883 | + } |
| 884 | + } |
| 885 | + |
| 886 | + // No body to adjudicate against (or detection abstained): trust the declaration. |
| 887 | + if (declared != null) { |
| 888 | + tryToSet7BitEncoding(msg, declared); |
| 889 | + } |
| 890 | + } |
| 891 | + |
| 892 | + /** |
| 893 | + * The charset a 7-bit message declares for itself: its codepage property |
| 894 | + * (MESSAGE_CODEPAGE / INTERNET_CPID), else a {@code charset} on a Content-Type |
| 895 | + * header. A hint for the detector, not a verdict. {@code null} if none. |
| 896 | + */ |
| 897 | + private static String declaredCharset(MAPIMessage msg, Chunks mainChunks) { |
851 | 898 | Map<MAPIProperty, List<PropertyValue>> props = mainChunks.getProperties(); |
852 | 899 | if (props != null) { |
853 | | - // First choice is a codepage property |
854 | | - for (MAPIProperty prop : new MAPIProperty[]{MAPIProperty.MESSAGE_CODEPAGE, MAPIProperty.INTERNET_CPID}) { |
| 900 | + for (MAPIProperty prop : new MAPIProperty[]{MAPIProperty.MESSAGE_CODEPAGE, |
| 901 | + MAPIProperty.INTERNET_CPID}) { |
855 | 902 | List<PropertyValue> val = props.get(prop); |
856 | 903 | if (val != null && val.size() > 0) { |
857 | 904 | int codepage = ((PropertyValue.LongPropertyValue) val.get(0)).getValue(); |
858 | | - String encoding = null; |
859 | 905 | try { |
860 | | - encoding = CodePageUtil.codepageToEncoding(codepage, true); |
861 | | - } catch (UnsupportedEncodingException e) { |
862 | | - //swallow |
863 | | - } |
864 | | - if (tryToSet7BitEncoding(msg, encoding)) { |
865 | | - return; |
| 906 | + String encoding = CodePageUtil.codepageToEncoding(codepage, true); |
| 907 | + if (encoding != null && Charset.isSupported(encoding)) { |
| 908 | + return encoding; |
| 909 | + } |
| 910 | + } catch (UnsupportedEncodingException | IllegalArgumentException e) { |
| 911 | + //swallow, try the next source |
866 | 912 | } |
867 | 913 | } |
868 | 914 | } |
869 | 915 | } |
870 | | - |
871 | | - // Second choice is a charset on a content type header |
872 | 916 | try { |
873 | 917 | String[] headers = msg.getHeaders(); |
874 | | - if (headers != null && headers.length > 0) { |
875 | | - // Look for a content type with a charset |
876 | | - Pattern p = Pattern.compile("Content-Type:.*?charset=[\"']?([^;'\"]+)[\"']?", Pattern.CASE_INSENSITIVE); |
877 | | - |
| 918 | + if (headers != null) { |
| 919 | + Pattern p = Pattern.compile( |
| 920 | + "Content-Type:.*?charset=[\"']?([^;'\"]+)[\"']?", Pattern.CASE_INSENSITIVE); |
878 | 921 | for (String header : headers) { |
879 | 922 | if (header.startsWith("Content-Type")) { |
880 | 923 | Matcher m = p.matcher(header); |
881 | 924 | if (m.matches()) { |
882 | | - // Found it! Tell all the string chunks |
883 | | - String charset = m.group(1); |
884 | | - if (tryToSet7BitEncoding(msg, charset)) { |
885 | | - return; |
886 | | - } |
| 925 | + return m.group(1); |
887 | 926 | } |
888 | 927 | } |
889 | 928 | } |
890 | 929 | } |
891 | 930 | } catch (ChunkNotFoundException e) { |
892 | 931 | //swallow |
893 | 932 | } |
894 | | - |
895 | | - // Nothing suitable in the headers, try HTML |
896 | | - // TODO: do we need to replicate this in Tika? If we wind up |
897 | | - // parsing the html version of the email, this is duplicative?? |
898 | | - // Or do we need to reset the header strings based on the html |
899 | | - // meta header if there is no other information? |
900 | | - try { |
901 | | - String html = msg.getHtmlBody(); |
902 | | - if (html != null && html.length() > 0) { |
903 | | - Charset charset = null; |
904 | | - try (TikaInputStream tis = TikaInputStream.get(html.getBytes(UTF_8))) { |
905 | | - List<EncodingResult> encResults = |
906 | | - detector.detect(tis, EMPTY_METADATA, context); |
907 | | - charset = encResults.isEmpty() ? null : encResults.get(0).getDecodeAs(); |
908 | | - } catch (IOException e) { |
909 | | - //swallow |
910 | | - } |
911 | | - if (charset != null && tryToSet7BitEncoding(msg, charset.name())) { |
912 | | - return; |
913 | | - } |
914 | | - } |
915 | | - } catch (ChunkNotFoundException e) { |
916 | | - //swallow |
917 | | - } |
918 | | - |
919 | | - //absolute last resort, try charset detector |
920 | | - StringChunk text = mainChunks.getTextBodyChunk(); |
921 | | - if (text != null) { |
922 | | - CharsetDetector detector = new CharsetDetector(); |
923 | | - detector.setText(text.getRawValue()); |
924 | | - CharsetMatch match = detector.detect(); |
925 | | - if (match != null && match.getConfidence() > 35) { |
926 | | - String charsetName = match.getName(); |
927 | | - try { |
928 | | - charsetName = CharsetSupersets.decodeAs(Charset.forName(charsetName)).name(); |
929 | | - } catch (IllegalArgumentException e) { |
930 | | - //ICU name not a resolvable Java charset; use as-is |
931 | | - } |
932 | | - if (tryToSet7BitEncoding(msg, charsetName)) { |
933 | | - return; |
934 | | - } |
935 | | - } |
936 | | - } |
| 933 | + return null; |
937 | 934 | } |
938 | 935 |
|
939 | 936 | private boolean tryToSet7BitEncoding(MAPIMessage msg, String charsetName) { |
|
0 commit comments