]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/osgi/BundleCmsTheme.java
Make tree view more robust
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / osgi / BundleCmsTheme.java
1 package org.argeo.cms.osgi;
2
3 import static java.nio.charset.StandardCharsets.UTF_8;
4
5 import java.io.BufferedReader;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.io.InputStreamReader;
9 import java.net.URL;
10 import java.util.ArrayList;
11 import java.util.Enumeration;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Set;
16 import java.util.TreeSet;
17 import java.util.stream.Collectors;
18
19 import org.argeo.api.cms.ux.CmsTheme;
20 import org.argeo.util.StreamUtils;
21 import org.osgi.framework.Bundle;
22 import org.osgi.framework.BundleContext;
23
24 /**
25 * Simplifies the theming of an app (only RAP is supported at this stage).<br>
26 *
27 * Additional fonts listed in <code>/fonts.txt</code>.<br>
28 * Additional (standard CSS) header in <code>/header.css</code>.<br>
29 * RAP specific CSS files in <code>/rap/*.css</code>.<br>
30 * All images added as additional resources based on extensions
31 * <code>/ ** /*.{png,gif,jpeg,...}</code>.<br>
32 */
33 public class BundleCmsTheme implements CmsTheme {
34 // public final static String DEFAULT_CMS_THEME_BUNDLE = "org.argeo.theme.argeo2";
35
36 // public final static String CMS_THEME_PROPERTY = "argeo.cms.theme";
37 @Deprecated
38 public final static String CMS_THEME_BUNDLE_PROPERTY = "argeo.cms.theme.bundle";
39
40 /** Declared theme ID, to be used by OSGi services to reference it as parent. */
41 public final static String THEME_ID_PROPERTY = "themeId";
42 public final static String SMALL_ICON_SIZE_PROPERTY = "smallIconSize";
43 public final static String BIG_ICON_SIZE_PROPERTY = "bigIconSize";
44
45 private final static String HEADER_CSS = "header.css";
46 private final static String FONTS_TXT = "fonts.txt";
47 private final static String BODY_HTML = "body.html";
48
49 // private final static Log log = LogFactory.getLog(BundleCmsTheme.class);
50
51 private CmsTheme parentTheme;
52
53 private String themeId;
54 private String declaredThemeId;;
55
56 private Set<String> webCssPaths = new TreeSet<>();
57 private Set<String> rapCssPaths = new TreeSet<>();
58 private Set<String> swtCssPaths = new TreeSet<>();
59 private Set<String> imagesPaths = new TreeSet<>();
60 private Set<String> fontsPaths = new TreeSet<>();
61
62 private String headerCss;
63 private List<String> fonts = new ArrayList<>();
64
65 private String bodyHtml = "<body></body>";
66
67 private String basePath;
68 private String styleCssPath;
69 // private String webCssPath;
70 // private String rapCssPath;
71 // private String swtCssPath;
72 private Bundle themeBundle;
73
74 private Integer smallIconSize = 16;
75 private Integer bigIconSize = 32;
76
77 public BundleCmsTheme() {
78
79 }
80
81 public void init(BundleContext bundleContext, Map<String, String> properties) {
82 declaredThemeId = properties.get(THEME_ID_PROPERTY);
83 if (properties.containsKey(SMALL_ICON_SIZE_PROPERTY))
84 smallIconSize = Integer.valueOf(properties.get(SMALL_ICON_SIZE_PROPERTY));
85 if (properties.containsKey(BIG_ICON_SIZE_PROPERTY))
86 smallIconSize = Integer.valueOf(properties.get(BIG_ICON_SIZE_PROPERTY));
87
88 initResources(bundleContext, null);
89 }
90
91 public void destroy(BundleContext bundleContext, Map<String, String> properties) {
92
93 }
94
95 @Deprecated
96 public BundleCmsTheme(BundleContext bundleContext) {
97 this(bundleContext, null);
98 }
99
100 @Deprecated
101 public BundleCmsTheme(BundleContext bundleContext, String symbolicName) {
102 initResources(bundleContext, symbolicName);
103 }
104
105 private void initResources(BundleContext bundleContext, String symbolicName) {
106 if (symbolicName == null) {
107 themeBundle = bundleContext.getBundle();
108 // basePath = "/theme/";
109 // cssPath = basePath;
110 } else {
111 themeBundle = findThemeBundle(bundleContext, symbolicName);
112 }
113 basePath = "/";
114 styleCssPath = "/style/";
115 // webCssPath = "/css/";
116 // rapCssPath = "/rap/";
117 // swtCssPath = "/swt/";
118 // this.themeId = RWT.DEFAULT_THEME_ID;
119 this.themeId = themeBundle.getSymbolicName();
120 if (declaredThemeId != null && !declaredThemeId.equals(themeId))
121 throw new IllegalArgumentException(
122 "Declared theme id " + declaredThemeId + " is different from " + themeId);
123
124 webCssPaths = addCss(themeBundle, "/css/");
125 rapCssPaths = addCss(themeBundle, "/rap/");
126 swtCssPaths = addCss(themeBundle, "/swt/");
127 addImages("*.png");
128 addImages("*.gif");
129 addImages("*.jpg");
130 addImages("*.jpeg");
131 addImages("*.svg");
132 addImages("*.ico");
133
134 addFonts("*.woff");
135 addFonts("*.woff2");
136
137 // fonts
138 URL fontsUrl = themeBundle.getEntry(basePath + FONTS_TXT);
139 if (fontsUrl != null) {
140 loadFontsUrl(fontsUrl);
141 }
142
143 // common CSS header (plain CSS)
144 URL headerCssUrl = themeBundle.getEntry(basePath + HEADER_CSS);
145 if (headerCssUrl != null) {
146 // added to plain Web CSS
147 webCssPaths.add(basePath + HEADER_CSS);
148 // and it will also be used by RAP:
149 try (BufferedReader buffer = new BufferedReader(new InputStreamReader(headerCssUrl.openStream(), UTF_8))) {
150 headerCss = buffer.lines().collect(Collectors.joining("\n"));
151 } catch (IOException e) {
152 throw new IllegalArgumentException("Cannot read " + headerCssUrl, e);
153 }
154 }
155
156 // body
157 URL bodyUrl = themeBundle.getEntry(basePath + BODY_HTML);
158 if (bodyUrl != null) {
159 loadBodyHtml(bodyUrl);
160 }
161 }
162
163 public String getHtmlHeaders() {
164 StringBuilder sb = new StringBuilder();
165 if (headerCss != null) {
166 sb.append("<style type='text/css'>\n");
167 sb.append(headerCss);
168 sb.append("\n</style>\n");
169 }
170 for (String link : fonts) {
171 sb.append("<link rel='stylesheet' href='");
172 sb.append(link);
173 sb.append("'/>\n");
174 }
175 if (sb.length() == 0)
176 return null;
177 else
178 return sb.toString();
179 }
180
181 @Override
182 public String getBodyHtml() {
183 return bodyHtml;
184 }
185
186 Set<String> addCss(Bundle themeBundle, String path) {
187 Set<String> paths = new TreeSet<>();
188
189 // common CSS
190 Enumeration<URL> commonResources = themeBundle.findEntries(styleCssPath, "*.css", true);
191 if (commonResources != null) {
192 while (commonResources.hasMoreElements()) {
193 String resource = commonResources.nextElement().getPath();
194 // remove first '/' so that RWT registers it
195 resource = resource.substring(1);
196 if (!resource.endsWith("/")) {
197 paths.add(resource);
198 }
199 }
200 }
201
202 // specific CSS
203 Enumeration<URL> themeResources = themeBundle.findEntries(path, "*.css", true);
204 if (themeResources != null) {
205 while (themeResources.hasMoreElements()) {
206 String resource = themeResources.nextElement().getPath();
207 // remove first '/' so that RWT registers it
208 resource = resource.substring(1);
209 if (!resource.endsWith("/")) {
210 paths.add(resource);
211 }
212 }
213 }
214 return paths;
215 }
216
217 void loadFontsUrl(URL url) {
218 try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), UTF_8))) {
219 String line = null;
220 while ((line = in.readLine()) != null) {
221 line = line.trim();
222 if (!line.equals("") && !line.startsWith("#")) {
223 fonts.add(line);
224 }
225 }
226 } catch (IOException e) {
227 throw new IllegalArgumentException("Cannot load URL " + url, e);
228 }
229 }
230
231 void loadBodyHtml(URL url) {
232 try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), UTF_8))) {
233 bodyHtml = StreamUtils.toString(in);
234 } catch (IOException e) {
235 throw new IllegalArgumentException("Cannot load URL " + url, e);
236 }
237 }
238
239 void addImages(String pattern) {
240 Enumeration<URL> themeResources = themeBundle.findEntries(basePath, pattern, true);
241 if (themeResources == null)
242 return;
243 while (themeResources.hasMoreElements()) {
244 String resource = themeResources.nextElement().getPath();
245 // remove first '/' so that RWT registers it
246 resource = resource.substring(1);
247 if (!resource.endsWith("/")) {
248 // if (resources.containsKey(resource))
249 // log.warn("Overriding " + resource + " from " + themeBundle.getSymbolicName());
250 // resources.put(resource, themeBRL);
251 imagesPaths.add(resource);
252 }
253
254 }
255
256 }
257
258 void addFonts(String pattern) {
259 Enumeration<URL> themeResources = themeBundle.findEntries(basePath, pattern, true);
260 if (themeResources == null)
261 return;
262 while (themeResources.hasMoreElements()) {
263 String resource = themeResources.nextElement().getPath();
264 // remove first '/' so that RWT registers it
265 resource = resource.substring(1);
266 if (!resource.endsWith("/")) {
267 // if (resources.containsKey(resource))
268 // log.warn("Overriding " + resource + " from " + themeBundle.getSymbolicName());
269 // resources.put(resource, themeBRL);
270 fontsPaths.add(resource);
271 }
272
273 }
274
275 }
276
277 @Override
278 public InputStream getResourceAsStream(String resourceName) throws IOException {
279 URL res = themeBundle.getEntry(resourceName);
280 if (res == null) {
281 res = themeBundle.getResource(resourceName);
282 if (res == null) {
283 if (parentTheme == null)
284 return null;
285 return parentTheme.getResourceAsStream(resourceName);
286 }
287 }
288 return res.openStream();
289 }
290
291 public String getThemeId() {
292 return themeId;
293 }
294
295 @Override
296 public Set<String> getWebCssPaths() {
297 if (parentTheme != null) {
298 Set<String> res = new HashSet<>(parentTheme.getWebCssPaths());
299 res.addAll(webCssPaths);
300 return res;
301 }
302 return webCssPaths;
303 }
304
305 @Override
306 public Set<String> getRapCssPaths() {
307 if (parentTheme != null) {
308 Set<String> res = new HashSet<>(parentTheme.getRapCssPaths());
309 res.addAll(rapCssPaths);
310 return res;
311 }
312 return rapCssPaths;
313 }
314
315 @Override
316 public Set<String> getSwtCssPaths() {
317 if (parentTheme != null) {
318 Set<String> res = new HashSet<>(parentTheme.getSwtCssPaths());
319 res.addAll(swtCssPaths);
320 return res;
321 }
322 return swtCssPaths;
323 }
324
325 @Override
326 public Set<String> getImagesPaths() {
327 if (parentTheme != null) {
328 Set<String> res = new HashSet<>(parentTheme.getImagesPaths());
329 res.addAll(imagesPaths);
330 return res;
331 }
332 return imagesPaths;
333 }
334
335 @Override
336 public Set<String> getFontsPaths() {
337 return fontsPaths;
338 }
339
340 @Override
341 public int getSmallIconSize() {
342 return smallIconSize;
343 }
344
345 @Override
346 public int getBigIconSize() {
347 return bigIconSize;
348 }
349
350 @Override
351 public InputStream loadPath(String path) throws IOException {
352 URL url = themeBundle.getResource(path);
353 if (url == null) {
354 if (parentTheme != null)
355 return parentTheme.loadPath(path);
356 else
357 throw new IllegalArgumentException(
358 "Path " + path + " not found in bundle " + themeBundle.getSymbolicName());
359 } else {
360 return url.openStream();
361 }
362 }
363
364 private static Bundle findThemeBundle(BundleContext bundleContext, String themeId) {
365 if (themeId == null)
366 return null;
367 // TODO optimize
368 // TODO deal with multiple versions
369 Bundle themeBundle = null;
370 if (themeId != null) {
371 for (Bundle bundle : bundleContext.getBundles())
372 if (themeId.equals(bundle.getSymbolicName())) {
373 themeBundle = bundle;
374 break;
375 }
376 }
377 return themeBundle;
378 }
379
380 @Override
381 public int hashCode() {
382 return themeId.hashCode();
383 }
384
385 @Override
386 public String toString() {
387 return "Bundle CMS Theme " + themeId;
388 }
389
390 public void setParentTheme(CmsTheme parentTheme) {
391 this.parentTheme = parentTheme;
392 }
393
394 public void setSmallIconSize(Integer smallIconSize) {
395 this.smallIconSize = smallIconSize;
396 }
397
398 public void setBigIconSize(Integer bigIconSize) {
399 this.bigIconSize = bigIconSize;
400 }
401
402 }