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}