001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 */
020package org.nuxeo.ecm.core.convert.service;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.regex.Pattern;
030import java.util.stream.Collectors;
031
032import org.apache.logging.log4j.LogManager;
033import org.apache.logging.log4j.Logger;
034import org.nuxeo.ecm.core.convert.extension.ConverterDescriptor;
035
036/**
037 * Helper class to manage chains of converters.
038 *
039 * @author tiry
040 */
041public class MimeTypeTranslationHelper {
042
043    private static final Logger log = LogManager.getLogger(MimeTypeTranslationHelper.class);
044
045    /**
046     * @since 10.3
047     */
048    public static final String ANY_MIME_TYPE = "*";
049
050    /**
051     * @since 10.3
052     */
053    public static final Pattern MIME_TYPE_PATTERN = Pattern.compile("(.*?);(.*)", Pattern.DOTALL);
054
055    protected final Map<String, List<ConvertOption>> srcMappings = new HashMap<>();
056
057    protected final Map<String, List<ConvertOption>> dstMappings = new HashMap<>();
058
059    public void addConverter(ConverterDescriptor desc) {
060        List<String> sMts = desc.getSourceMimeTypes();
061        String dMt = desc.getDestinationMimeType();
062
063        List<ConvertOption> dco = dstMappings.computeIfAbsent(dMt, key -> new ArrayList<>());
064        for (String sMT : sMts) {
065            List<ConvertOption> sco = srcMappings.computeIfAbsent(sMT, key -> new ArrayList<>());
066            sco.add(new ConvertOption(desc.getConverterName(), dMt));
067            dco.add(new ConvertOption(desc.getConverterName(), sMT));
068        }
069        log.debug("Added converter {} to {}", desc::getSourceMimeTypes, desc::getDestinationMimeType);
070    }
071
072    /**
073     * Returns the last registered converter name for the given {@code sourceMimeType} and {@code destinationMimeType}.
074     * <p>
075     * Follows the algorithm of {@link #getConverterNames(String, String)}.
076     *
077     * @see #getConverterNames(String, String)
078     * @see #getConverterName(String, String, boolean)
079     */
080    public String getConverterName(String sourceMimeType, String destinationMimeType) {
081        return getConverterName(sourceMimeType, destinationMimeType, true);
082    }
083
084    /**
085     * Returns the last registered converter name for the given {@code sourceMimeType} and {@code destinationMimeType}.
086     * <p>
087     * Follows the algorithm of {@link #getConverterNames(String, String, boolean)}.
088     *
089     * @since 11.1
090     * @see #getConverterNames(String, String, boolean)
091     */
092    public String getConverterName(String sourceMimeType, String destinationMimeType, boolean allowWildcard) {
093        List<String> converterNames = getConverterNames(sourceMimeType, destinationMimeType, allowWildcard);
094        return converterNames.isEmpty() ? null : converterNames.get(converterNames.size() - 1);
095    }
096
097    /**
098     * Returns {@code true} if the given {@code mimeTypes} has a compatible mime type with {@code mimeType},
099     * {@code false} otherwise.
100     * <p>
101     * The {@code mimeTypes} list has a compatible mime type if:
102     * <ul>
103     * <li>it contains "*"</li>
104     * <li>it contains exactly {@code mimeType}</li>
105     * <li>it contains a mime type with the same primary type as {@code mimeType} and a wildcard sub type</li>
106     * </ul>
107     *
108     * @since 10.3
109     */
110    public boolean hasCompatibleMimeType(List<String> mimeTypes, String mimeType) {
111        String mt = parseMimeType(mimeType);
112        Set<String> expectedMimeTypes = new HashSet<>();
113        expectedMimeTypes.add(ANY_MIME_TYPE);
114        if (mt != null) {
115            expectedMimeTypes.add(mt);
116            expectedMimeTypes.add(computeMimeTypeWithWildcardSubType(mt));
117        }
118        return mimeTypes.stream().anyMatch(expectedMimeTypes::contains);
119    }
120
121    /**
122     * Returns the list of converter names handling the given {@code sourceMimeType} and {@code destinationMimeType}.
123     *
124     * @see #getConverterNames(String, String, boolean)
125     */
126    public List<String> getConverterNames(String sourceMimeType, String destinationMimeType) {
127        return getConverterNames(sourceMimeType, destinationMimeType, true);
128    }
129
130    /**
131     * Returns the list of converter names handling the given {@code sourceMimeType} and {@code destinationMimeType}.
132     * <p>
133     * Finds the converter names based on the following algorithm:
134     * <ul>
135     * <li>Find the converters exactly matching the given {@code sourceMimeType}</li>
136     * <li>If no converter found, find the converters matching a wildcard subtype based on the {@code sourceMimeType},
137     * such has "image/*"</li>
138     * <li>If no converter found and {@code allowWildcard} is {@code true}, find the converters matching a wildcard
139     * source mime type "*"</li>
140     * <li>Then, filter only the converters matching the given {@code destinationMimeType}</li>
141     * </ul>
142     *
143     * @param allowWildcard {@code true} to allow returning converters with '*' as source mime type.
144     * @since 11.1
145     */
146    public List<String> getConverterNames(String sourceMimeType, String destinationMimeType, boolean allowWildcard) {
147        // remove content type parameters if any
148        String srcMimeType = parseMimeType(sourceMimeType);
149
150        List<ConvertOption> cos = srcMappings.getOrDefault(srcMimeType, Collections.emptyList());
151        if (cos.isEmpty()) {
152            // use a mime type with a wildcard sub type
153            cos = srcMappings.getOrDefault(computeMimeTypeWithWildcardSubType(srcMimeType), Collections.emptyList());
154        }
155
156        if (cos.isEmpty() && allowWildcard) {
157            // use a wildcard mime type
158            cos = srcMappings.getOrDefault(ANY_MIME_TYPE, Collections.emptyList());
159        }
160
161        return cos.stream()
162                  .filter(co -> destinationMimeType == null || destinationMimeType.equals(co.mimeType))
163                  .map(co -> co.converter)
164                  .collect(Collectors.toList());
165    }
166
167    /**
168     * Parses the given {@code mimeType} and returns only the primary type and optionally the sub type if any.
169     * <p>
170     * Some input/output samples:
171     * <ul>
172     * <li>"image/jpeg" =&gt; "image/jpeg"</li>
173     * <li>"image/*" =&gt; "image/*"</li>
174     * <li>"image/png; param1=foo; param2=bar" =&gt; "image/png"</li>
175     * </ul>
176     *
177     * @since 10.3
178     */
179    protected String parseMimeType(String mimeType) {
180        if (mimeType == null) {
181            return null;
182        }
183
184        return MIME_TYPE_PATTERN.matcher(mimeType).replaceAll("$1").trim();
185    }
186
187    /**
188     * Returns a new mime type with the primary type of the given {@code mimeType} and a wildcard sub type.
189     * <p>
190     * Some input/output samples:
191     * <ul>
192     * <li>"image/jpeg" =&gt; "image/*"</li>
193     * <li>"video/*" =&gt; "video/*"</li>
194     * <li>"application/pdf" =&gt; "application/*"</li>
195     * </ul>
196     *
197     * @since 10.3
198     */
199    protected String computeMimeTypeWithWildcardSubType(String mimeType) {
200        return mimeType != null ? mimeType.replaceAll("(.*)/(.*)", "$1/" + ANY_MIME_TYPE) : null;
201    }
202
203    /**
204     * @deprecated since 10.3. Not used.
205     */
206    @Deprecated
207    public List<String> getDestinationMimeTypes(String sourceMimeType) {
208        List<String> dst = new ArrayList<>();
209
210        List<ConvertOption> sco = srcMappings.get(sourceMimeType);
211
212        if (sco != null) {
213            for (ConvertOption co : sco) {
214                dst.add(co.getMimeType());
215            }
216        }
217        return dst;
218    }
219
220    /**
221     * @deprecated since 10.3. Not used.
222     */
223    @Deprecated
224    public List<String> getSourceMimeTypes(String destinationMimeType) {
225        List<String> src = new ArrayList<>();
226
227        List<ConvertOption> dco = dstMappings.get(destinationMimeType);
228
229        if (dco != null) {
230            for (ConvertOption co : dco) {
231                src.add(co.getMimeType());
232            }
233        }
234        return src;
235    }
236
237    public void clear() {
238        dstMappings.clear();
239        srcMappings.clear();
240        log.debug("clear");
241    }
242
243}