001/**
002 * Copyright (C) 2006-2020 Talend Inc. - www.talend.com
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 */
016package org.talend.sdk.component.server.service;
017
018import static java.util.Locale.ROOT;
019import static java.util.Optional.of;
020import static java.util.Optional.ofNullable;
021import static java.util.function.Function.identity;
022import static java.util.stream.Collectors.toList;
023
024import java.io.BufferedInputStream;
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.util.Collection;
029import java.util.List;
030import java.util.Optional;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ConcurrentMap;
033
034import javax.annotation.PostConstruct;
035import javax.enterprise.context.ApplicationScoped;
036import javax.inject.Inject;
037
038import org.talend.sdk.component.container.Container;
039import org.talend.sdk.component.server.configuration.ComponentServerConfiguration;
040
041import lombok.Data;
042
043@ApplicationScoped
044public class IconResolver {
045
046    @Inject
047    private ComponentServerConfiguration componentServerConfiguration;
048
049    private boolean supportsSvg;
050
051    private List<String> patterns;
052
053    @PostConstruct
054    protected void init() {
055        supportsSvg = System.getProperty("talend.studio.version") == null
056                && componentServerConfiguration.getIconExtensions().stream().anyMatch(it -> it.endsWith(".svg"));
057        patterns = isSupportsSvg() ? componentServerConfiguration.getIconExtensions()
058                : componentServerConfiguration
059                        .getIconExtensions()
060                        .stream()
061                        .filter(it -> !it.endsWith(".svg"))
062                        .collect(toList());
063    }
064
065    protected boolean isSupportsSvg() {
066        return supportsSvg;
067    }
068
069    protected Collection<String> getExtensionPreferences() {
070        return patterns;
071    }
072
073    /**
074     * IMPORTANT: the strategy moved to the configuration, high level we want something in this spirit:
075     *
076     * The look up strategy of an icon is the following one:
077     * 1. Check in the server classpath in icons/override/${icon}_icon32.png
078     * (optionally icons/override/${icon}.svg if in the preferences),
079     * 2. Check in the family classloader the following names ${icon}_icon32.png, icons/${icon}_icon32.png, ...
080     * 3. Check in the server classloader the following names ${icon}_icon32.png, icons/${icon}_icon32.png, ...
081     *
082     * This enable to
083     * 1. override properly the icons (1),
084     * 2. provide them in the family (2) and
085     * 3. fallback on built-in icons if needed (3).
086     *
087     * @param container the component family container.
088     * @param icon the icon to look up.
089     * @return the icon if found.
090     */
091    public Icon resolve(final Container container, final String icon) {
092        if (icon == null) {
093            return null;
094        }
095
096        Cache cache = container.get(Cache.class);
097        if (cache == null) {
098            synchronized (container) {
099                cache = container.get(Cache.class);
100                if (cache == null) {
101                    cache = new Cache();
102                    container.set(Cache.class, cache);
103                }
104            }
105        }
106        final ClassLoader appLoader = Thread.currentThread().getContextClassLoader();
107        return cache.icons
108                .computeIfAbsent(icon,
109                        k -> ofNullable(getOverridenIcon(icon, appLoader)
110                                .orElseGet(() -> doLoad(container.getLoader(), icon)
111                                        .orElseGet(() -> doLoad(appLoader, icon).orElse(null)))))
112                .orElse(null);
113    }
114
115    private Optional<Icon> getOverridenIcon(final String icon, final ClassLoader appLoader) {
116        Icon result = null;
117        if (isSupportsSvg()) {
118            result = loadIcon(appLoader, "icons/override/" + icon + ".svg").orElse(null);
119        }
120        if (result == null) {
121            return loadIcon(appLoader, "icons/override/" + icon + "_icon32.png");
122        }
123        return of(result);
124    }
125
126    public Optional<Icon> doLoad(final ClassLoader loader, final String icon) {
127        return getExtensionPreferences()
128                .stream()
129                .map(ext -> String.format(ext, icon))
130                .map(path -> loadIcon(loader, path))
131                .filter(Optional::isPresent)
132                .findFirst()
133                .flatMap(identity());
134    }
135
136    private Optional<Icon> loadIcon(final ClassLoader loader, final String path) {
137        return ofNullable(loader.getResourceAsStream(path))
138                .map(resource -> new Icon(getType(path.toLowerCase(ROOT)), toBytes(resource)));
139    }
140
141    private String getType(final String path) {
142        if (path.endsWith(".png")) {
143            return "image/png";
144        }
145        if (path.endsWith(".svg")) {
146            return "image/svg+xml";
147        }
148        throw new IllegalArgumentException("Unsupported icon type: " + path);
149    }
150
151    private static class Cache {
152
153        private final ConcurrentMap<String, Optional<Icon>> icons = new ConcurrentHashMap<>();
154    }
155
156    @Data
157    public static class Icon {
158
159        private final String type;
160
161        private final byte[] bytes;
162    }
163
164    private byte[] toBytes(final InputStream resource) {
165        try (final BufferedInputStream stream = new BufferedInputStream(resource)) {
166            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(stream.available());
167            final byte[] buffer = new byte[1024];
168            int read;
169            while ((read = stream.read(buffer, 0, buffer.length)) >= 0) {
170                if (read > 0) {
171                    byteArrayOutputStream.write(buffer, 0, read);
172                }
173            }
174            return byteArrayOutputStream.toByteArray();
175        } catch (final IOException e) {
176            throw new IllegalStateException(e);
177        }
178    }
179}