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     * Follow the algorithm of {@link #getConverterNames(String, String)}.
076     *
077     * @see #getConverterNames(String, String)
078     */
079    public String getConverterName(String sourceMimeType, String destinationMimeType) {
080        List<String> converterNames = getConverterNames(sourceMimeType, destinationMimeType);
081        return converterNames.isEmpty() ? null : converterNames.get(converterNames.size() - 1);
082    }
083
084    /**
085     * Returns {@code true} if the given {@code mimeTypes} has a compatible mime type with {@code mimeType},
086     * {@code false} otherwise.
087     * <p>
088     * The {@code mimeTypes} list has a compatible mime type if:
089     * <ul>
090     * <li>it contains "*"</li>
091     * <li>it contains exactly {@code mimeType}</li>
092     * <li>it contains a mime type with the same primary type as {@code mimeType} and a wildcard sub type</li>
093     * </ul>
094     *
095     * @since 10.3
096     */
097    public boolean hasCompatibleMimeType(List<String> mimeTypes, String mimeType) {
098        String mt = parseMimeType(mimeType);
099        Set<String> expectedMimeTypes = new HashSet<>();
100        expectedMimeTypes.add(ANY_MIME_TYPE);
101        if (mt != null) {
102            expectedMimeTypes.add(mt);
103            expectedMimeTypes.add(computeMimeTypeWithWildcardSubType(mt));
104        }
105        return mimeTypes.stream().anyMatch(expectedMimeTypes::contains);
106    }
107
108    /**
109     * Returns the list of converter names handling the given {@code sourceMimeType} and {@code destinationMimeType}.
110     * <p>
111     * Find the converter names based on the following algorithm:
112     * <ul>
113     * <li>Find the converters exactly matching the given {@code sourceMimeType}</li>
114     * <li>If no converter found, find the converters matching a wildcard subtype based on the {@code sourceMimeType},
115     * such has "image/*"</li>
116     * <li>If no converter found, find the converters matching a wildcard source mime type "*"</li>
117     * <li>Then, filter only the converting matching the given {@code destinationMimeType}</li>
118     * </ul>
119     */
120    public List<String> getConverterNames(String sourceMimeType, String destinationMimeType) {
121        // remove content type parameters if any
122        String srcMimeType = parseMimeType(sourceMimeType);
123
124        List<ConvertOption> cos = srcMappings.getOrDefault(srcMimeType, Collections.emptyList());
125        if (cos.isEmpty()) {
126            // use a mime type with a wildcard sub type
127            cos = srcMappings.getOrDefault(computeMimeTypeWithWildcardSubType(srcMimeType), Collections.emptyList());
128        }
129
130        if (cos.isEmpty()) {
131            // use a wildcard mime type
132            cos = srcMappings.getOrDefault(ANY_MIME_TYPE, Collections.emptyList());
133        }
134
135        return cos.stream()
136                  .filter(co -> destinationMimeType == null || co.mimeType.equals(destinationMimeType))
137                  .map(co -> co.converter)
138                  .collect(Collectors.toList());
139    }
140
141    /**
142     * Parses the given {@code mimeType} and returns only the primary type and optionally the sub type if any.
143     * <p>
144     * Some input/output samples:
145     * <ul>
146     * <li>"image/jpeg" => "image/jpeg"</li>
147     * <li>"image/*" => "image/*"</li>
148     * <li>"image/png; param1=foo; param2=bar" => "image/png"</li>
149     * </ul>
150     *
151     * @since 10.3
152     */
153    protected String parseMimeType(String mimeType) {
154        if (mimeType == null) {
155            return null;
156        }
157
158        return MIME_TYPE_PATTERN.matcher(mimeType).replaceAll("$1").trim();
159    }
160
161    /**
162     * Returns a new mime type with the primary type of the given {@code mimeType} and a wildcard sub type.
163     * <p>
164     * Some input/output samples:
165     * <ul>
166     * <li>"image/jpeg" => "image/*"</li>
167     * <li>"video/*" => "video/*"</li>
168     * <li>"application/pdf" => "application/*"</li>
169     * </ul>
170     *
171     * @since 10.3
172     */
173    protected String computeMimeTypeWithWildcardSubType(String mimeType) {
174        return mimeType != null ? mimeType.replaceAll("(.*)/(.*)", "$1/" + ANY_MIME_TYPE) : null;
175    }
176
177    /**
178     * @deprecated since 10.3. Not used.
179     */
180    @Deprecated
181    public List<String> getDestinationMimeTypes(String sourceMimeType) {
182        List<String> dst = new ArrayList<>();
183
184        List<ConvertOption> sco = srcMappings.get(sourceMimeType);
185
186        if (sco != null) {
187            for (ConvertOption co : sco) {
188                dst.add(co.getMimeType());
189            }
190        }
191        return dst;
192    }
193
194    /**
195     * @deprecated since 10.3. Not used.
196     */
197    @Deprecated
198    public List<String> getSourceMimeTypes(String destinationMimeType) {
199        List<String> src = new ArrayList<>();
200
201        List<ConvertOption> dco = dstMappings.get(destinationMimeType);
202
203        if (dco != null) {
204            for (ConvertOption co : dco) {
205                src.add(co.getMimeType());
206            }
207        }
208        return src;
209    }
210
211    public void clear() {
212        dstMappings.clear();
213        srcMappings.clear();
214        log.debug("clear");
215    }
216
217}