001/* 002 * (C) Copyright 2011-2018 Nuxeo (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 * Anahide Tchertchian 018 */ 019package org.nuxeo.theme.styling.service; 020 021import java.io.IOException; 022import java.net.MalformedURLException; 023import java.net.URL; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029 030import org.apache.commons.io.IOUtils; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.nuxeo.ecm.web.resources.api.Resource; 035import org.nuxeo.ecm.web.resources.api.ResourceType; 036import org.nuxeo.ecm.web.resources.api.service.WebResourceManager; 037import org.nuxeo.ecm.web.resources.core.ResourceDescriptor; 038import org.nuxeo.runtime.RuntimeMessage.Level; 039import org.nuxeo.runtime.RuntimeMessage.Source; 040import org.nuxeo.runtime.api.Framework; 041import org.nuxeo.runtime.logging.DeprecationLogger; 042import org.nuxeo.runtime.model.ComponentContext; 043import org.nuxeo.runtime.model.ComponentInstance; 044import org.nuxeo.runtime.model.ComponentName; 045import org.nuxeo.runtime.model.DefaultComponent; 046import org.nuxeo.runtime.model.RuntimeContext; 047import org.nuxeo.theme.styling.negotiation.Negotiator; 048import org.nuxeo.theme.styling.service.descriptors.FlavorDescriptor; 049import org.nuxeo.theme.styling.service.descriptors.FlavorPresets; 050import org.nuxeo.theme.styling.service.descriptors.IconDescriptor; 051import org.nuxeo.theme.styling.service.descriptors.LogoDescriptor; 052import org.nuxeo.theme.styling.service.descriptors.NegotiationDescriptor; 053import org.nuxeo.theme.styling.service.descriptors.NegotiatorDescriptor; 054import org.nuxeo.theme.styling.service.descriptors.PageDescriptor; 055import org.nuxeo.theme.styling.service.descriptors.PalettePreview; 056import org.nuxeo.theme.styling.service.descriptors.SassImport; 057import org.nuxeo.theme.styling.service.descriptors.SimpleStyle; 058import org.nuxeo.theme.styling.service.palettes.PaletteParseException; 059import org.nuxeo.theme.styling.service.palettes.PaletteParser; 060import org.nuxeo.theme.styling.service.registries.FlavorRegistry; 061import org.nuxeo.theme.styling.service.registries.NegotiationRegistry; 062import org.nuxeo.theme.styling.service.registries.PageRegistry; 063 064/** 065 * Default implementation for the {@link ThemeStylingService} 066 * 067 * @since 5.5 068 */ 069public class ThemeStylingServiceImpl extends DefaultComponent implements ThemeStylingService { 070 071 private static final Log log = LogFactory.getLog(ThemeStylingServiceImpl.class); 072 073 protected static final String WR_EX = "org.nuxeo.ecm.platform.WebResources"; 074 075 protected PageRegistry pageReg; 076 077 protected FlavorRegistry flavorReg; 078 079 protected NegotiationRegistry negReg; 080 081 // Runtime Component API 082 083 @Override 084 public void activate(ComponentContext context) { 085 super.activate(context); 086 pageReg = new PageRegistry(); 087 flavorReg = new FlavorRegistry(); 088 negReg = new NegotiationRegistry(); 089 } 090 091 @Override 092 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 093 if (contribution instanceof FlavorDescriptor) { 094 FlavorDescriptor flavor = (FlavorDescriptor) contribution; 095 log.info(String.format("Register flavor '%s'", flavor.getName())); 096 registerFlavor(flavor, contributor.getContext()); 097 log.info(String.format("Done registering flavor '%s'", flavor.getName())); 098 } else if (contribution instanceof SimpleStyle) { 099 SimpleStyle style = (SimpleStyle) contribution; 100 log.info(String.format("Register style '%s'", style.getName())); 101 ComponentName compName = contributor.getName(); 102 String message = String.format( 103 "Style '%s' on component %s should now be contributed to extension " 104 + "point '%s': a compatibility registration was performed but it may not be " 105 + "accurate. Note that the 'flavor' processor should be used with this resource.", 106 style.getName(), compName, WR_EX); 107 DeprecationLogger.log(message, "7.4"); 108 addRuntimeMessage(Level.WARNING, message, Source.EXTENSION, compName.getName()); 109 ResourceDescriptor resource = getResourceFromStyle(style); 110 registerResource(resource, contributor.getContext()); 111 log.info(String.format("Done registering style '%s'", style.getName())); 112 } else if (contribution instanceof PageDescriptor) { 113 PageDescriptor page = (PageDescriptor) contribution; 114 log.info(String.format("Register page '%s'", page.getName())); 115 if (page.hasResources()) { 116 // automatically register a bundle for page resources 117 WebResourceManager wrm = Framework.getService(WebResourceManager.class); 118 wrm.registerResourceBundle(page.getComputedResourceBundle()); 119 } 120 pageReg.addContribution(page); 121 log.info(String.format("Done registering page '%s'", page.getName())); 122 } else if (contribution instanceof ResourceDescriptor) { 123 ResourceDescriptor resource = (ResourceDescriptor) contribution; 124 log.info(String.format("Register resource '%s'", resource.getName())); 125 ComponentName compName = contributor.getName(); 126 String message = String.format( 127 "Resource '%s' on component %s should now be contributed to extension " 128 + "point '%s': a compatibility registration was performed but it may not be accurate.", 129 resource.getName(), compName, WR_EX); 130 DeprecationLogger.log(message, "7.4"); 131 addRuntimeMessage(Level.WARNING, message, Source.EXTENSION, compName.getName()); 132 // ensure path is absolute, consider that resource is in the war, and if not, user will have to declare it 133 // directly to the WRM endpoint 134 String path = resource.getPath(); 135 if (path != null && !path.startsWith("/")) { 136 resource.setUri("/" + path); 137 } 138 registerResource(resource, contributor.getContext()); 139 log.info(String.format("Done registering resource '%s'", resource.getName())); 140 } else if (contribution instanceof NegotiationDescriptor) { 141 NegotiationDescriptor neg = (NegotiationDescriptor) contribution; 142 log.info(String.format("Register negotiation for '%s'", neg.getTarget())); 143 negReg.addContribution(neg); 144 log.info(String.format("Done registering negotiation for '%s'", neg.getTarget())); 145 } else { 146 log.error(String.format("Unknown contribution to the theme " + "styling service, extension point '%s': '%s", 147 extensionPoint, contribution)); 148 } 149 } 150 151 @Override 152 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 153 if (contribution instanceof FlavorDescriptor) { 154 FlavorDescriptor flavor = (FlavorDescriptor) contribution; 155 flavorReg.removeContribution(flavor); 156 } else if (contribution instanceof Resource) { 157 Resource resource = (Resource) contribution; 158 unregisterResource(resource); 159 } else if (contribution instanceof SimpleStyle) { 160 SimpleStyle style = (SimpleStyle) contribution; 161 unregisterResource(getResourceFromStyle(style)); 162 } else if (contribution instanceof PageDescriptor) { 163 PageDescriptor page = (PageDescriptor) contribution; 164 if (page.hasResources()) { 165 WebResourceManager wrm = Framework.getService(WebResourceManager.class); 166 wrm.unregisterResourceBundle(page.getComputedResourceBundle()); 167 } 168 pageReg.removeContribution(page); 169 } else if (contribution instanceof NegotiationDescriptor) { 170 NegotiationDescriptor neg = (NegotiationDescriptor) contribution; 171 negReg.removeContribution(neg); 172 } else { 173 log.error(String.format("Unknown contribution to the theme " + "styling service, extension point '%s': '%s", 174 extensionPoint, contribution)); 175 } 176 } 177 178 protected void registerFlavor(FlavorDescriptor flavor, RuntimeContext extensionContext) { 179 // set flavor presets files content 180 List<FlavorPresets> presets = flavor.getPresets(); 181 if (presets != null) { 182 for (FlavorPresets myPreset : presets) { 183 String src = myPreset.getSrc(); 184 URL url = getUrlFromPath(src, extensionContext); 185 if (url == null) { 186 log.error(String.format("Could not find resource at '%s'", src)); 187 } else { 188 String content; 189 try { 190 content = new String(IOUtils.toByteArray(url)); 191 } catch (IOException e) { 192 throw new RuntimeException(e); 193 } 194 myPreset.setContent(content); 195 } 196 } 197 } 198 199 // set flavor sass variables 200 List<SassImport> sassVars = flavor.getSassImports(); 201 if (sassVars != null) { 202 for (SassImport var : sassVars) { 203 String src = var.getSrc(); 204 URL url = getUrlFromPath(src, extensionContext); 205 if (url == null) { 206 log.error(String.format("Could not find resource at '%s'", src)); 207 } else { 208 String content; 209 try { 210 content = new String(IOUtils.toByteArray(url)); 211 } catch (IOException e) { 212 throw new RuntimeException(e); 213 } 214 var.setContent(content); 215 } 216 } 217 } 218 219 flavorReg.addContribution(flavor); 220 } 221 222 protected List<FlavorPresets> computePresets(FlavorDescriptor flavor, List<String> flavors) { 223 List<FlavorPresets> presets = new ArrayList<>(); 224 if (flavor != null) { 225 List<FlavorPresets> localPresets = flavor.getPresets(); 226 if (localPresets != null) { 227 presets.addAll(localPresets); 228 } 229 String extendsFlavorName = flavor.getExtendsFlavor(); 230 if (!StringUtils.isBlank(extendsFlavorName)) { 231 if (flavors.contains(extendsFlavorName)) { 232 // cyclic dependency => abort 233 log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); 234 return presets; 235 } else { 236 // retrieve the extended presets 237 flavors.add(flavor.getName()); 238 FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); 239 if (extendedFlavor != null) { 240 List<FlavorPresets> parentPresets = computePresets(extendedFlavor, flavors); 241 if (parentPresets != null) { 242 presets.addAll(0, parentPresets); 243 } 244 } else { 245 log.warn("Extended flavor '" + extendsFlavorName + "' not found"); 246 } 247 } 248 } 249 } 250 return presets; 251 } 252 253 protected void registerResource(Resource resource, RuntimeContext extensionContext) { 254 WebResourceManager wrm = Framework.getService(WebResourceManager.class); 255 wrm.registerResource(resource); 256 } 257 258 protected void unregisterResource(Resource resource) { 259 // unregister directly to the WebResourceManager service 260 WebResourceManager wrm = Framework.getService(WebResourceManager.class); 261 wrm.unregisterResource(resource); 262 } 263 264 protected ResourceDescriptor getResourceFromStyle(SimpleStyle style) { 265 // turn style into a resource 266 ResourceDescriptor resource = new ResourceDescriptor(); 267 resource.setPath(style.getSrc()); 268 String name = style.getName(); 269 if (name.endsWith(ResourceType.css.name())) { 270 resource.setName(name); 271 } else { 272 resource.setName(name + "." + ResourceType.css.name()); 273 } 274 resource.setProcessors(Collections.singletonList("flavor")); 275 return resource; 276 } 277 278 protected URL getUrlFromPath(String path, RuntimeContext extensionContext) { 279 if (path == null) { 280 return null; 281 } 282 URL url; 283 try { 284 url = new URL(path); 285 } catch (MalformedURLException e) { 286 url = extensionContext.getLocalResource(path); 287 if (url == null) { 288 url = extensionContext.getResource(path); 289 } 290 } 291 return url; 292 } 293 294 // service API 295 296 @Override 297 public String getDefaultFlavorName(String themePageName) { 298 if (pageReg != null) { 299 PageDescriptor themePage = pageReg.getPage(themePageName); 300 if (themePage != null) { 301 return themePage.getDefaultFlavor(); 302 } 303 } 304 return null; 305 } 306 307 @Override 308 public FlavorDescriptor getFlavor(String flavorName) { 309 if (flavorReg != null) { 310 FlavorDescriptor flavor = flavorReg.getFlavor(flavorName); 311 if (flavor != null) { 312 FlavorDescriptor clone = flavor.clone(); 313 clone.setLogo(computeLogo(flavor, new ArrayList<>())); 314 clone.setPalettePreview(computePalettePreview(flavor, new ArrayList<>())); 315 clone.setFavicons(computeIcons(flavor, new ArrayList<>())); 316 return clone; 317 } 318 } 319 return null; 320 } 321 322 @Override 323 public LogoDescriptor getLogo(String flavorName) { 324 FlavorDescriptor flavor = getFlavor(flavorName); 325 if (flavor != null) { 326 return flavor.getLogo(); 327 } 328 return null; 329 } 330 331 protected LogoDescriptor computeLogo(FlavorDescriptor flavor, List<String> flavors) { 332 if (flavor != null) { 333 LogoDescriptor localLogo = flavor.getLogo(); 334 if (localLogo == null) { 335 String extendsFlavorName = flavor.getExtendsFlavor(); 336 if (!StringUtils.isBlank(extendsFlavorName)) { 337 if (flavors.contains(extendsFlavorName)) { 338 // cyclic dependency => abort 339 log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); 340 return null; 341 } else { 342 // retrieved the extended logo 343 flavors.add(flavor.getName()); 344 FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); 345 if (extendedFlavor != null) { 346 localLogo = computeLogo(extendedFlavor, flavors); 347 } else { 348 log.warn("Extended flavor '" + extendsFlavorName + "' not found"); 349 } 350 } 351 } 352 } 353 return localLogo; 354 } 355 return null; 356 } 357 358 protected PalettePreview computePalettePreview(FlavorDescriptor flavor, List<String> flavors) { 359 if (flavor != null) { 360 PalettePreview localPalette = flavor.getPalettePreview(); 361 if (localPalette == null) { 362 String extendsFlavorName = flavor.getExtendsFlavor(); 363 if (!StringUtils.isBlank(extendsFlavorName)) { 364 if (flavors.contains(extendsFlavorName)) { 365 // cyclic dependency => abort 366 log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); 367 return null; 368 } else { 369 // retrieved the extended colors 370 flavors.add(flavor.getName()); 371 FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); 372 if (extendedFlavor != null) { 373 localPalette = computePalettePreview(extendedFlavor, flavors); 374 } else { 375 log.warn("Extended flavor '" + extendsFlavorName + "' not found"); 376 } 377 } 378 } 379 } 380 return localPalette; 381 } 382 return null; 383 } 384 385 protected List<IconDescriptor> computeIcons(FlavorDescriptor flavor, List<String> flavors) { 386 if (flavor != null) { 387 List<IconDescriptor> localIcons = flavor.getFavicons(); 388 if (localIcons == null || localIcons.isEmpty()) { 389 String extendsFlavorName = flavor.getExtendsFlavor(); 390 if (!StringUtils.isBlank(extendsFlavorName)) { 391 if (flavors.contains(extendsFlavorName)) { 392 // cyclic dependency => abort 393 log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); 394 return null; 395 } else { 396 // retrieved the extended icons 397 flavors.add(flavor.getName()); 398 FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); 399 if (extendedFlavor != null) { 400 localIcons = computeIcons(extendedFlavor, flavors); 401 } else { 402 log.warn("Extended flavor '" + extendsFlavorName + "' not found"); 403 } 404 } 405 } 406 } 407 return localIcons; 408 } 409 return null; 410 } 411 412 @Override 413 public List<String> getFlavorNames(String themePageName) { 414 if (pageReg != null) { 415 PageDescriptor themePage = pageReg.getPage(themePageName); 416 if (themePage != null) { 417 List<String> flavors = new ArrayList<>(); 418 List<String> localFlavors = themePage.getFlavors(); 419 if (localFlavors != null) { 420 flavors.addAll(localFlavors); 421 } 422 // add flavors from theme for all pages 423 PageDescriptor forAllPage = pageReg.getConfigurationApplyingToAll(); 424 if (forAllPage != null) { 425 localFlavors = forAllPage.getFlavors(); 426 if (localFlavors != null) { 427 flavors.addAll(localFlavors); 428 } 429 } 430 // add default flavor if it's not listed there 431 String defaultFlavor = themePage.getDefaultFlavor(); 432 if (defaultFlavor != null) { 433 if (!flavors.contains(defaultFlavor)) { 434 flavors.add(0, defaultFlavor); 435 } 436 } 437 return flavors; 438 } 439 } 440 return null; 441 } 442 443 @Override 444 public List<FlavorDescriptor> getFlavors(String themePageName) { 445 List<String> flavorNames = getFlavorNames(themePageName); 446 if (flavorNames != null) { 447 List<FlavorDescriptor> flavors = new ArrayList<>(); 448 for (String flavorName : flavorNames) { 449 FlavorDescriptor flavor = getFlavor(flavorName); 450 if (flavor != null) { 451 flavors.add(flavor); 452 } 453 } 454 return flavors; 455 } 456 return null; 457 } 458 459 protected Map<String, Map<String, String>> getPresetsByCat(FlavorDescriptor flavor) { 460 String flavorName = flavor.getName(); 461 List<FlavorPresets> presets = computePresets(flavor, new ArrayList<>()); 462 Map<String, Map<String, String>> presetsByCat = new HashMap<>(); 463 if (presets != null) { 464 for (FlavorPresets myPreset : presets) { 465 String content = myPreset.getContent(); 466 if (content == null) { 467 log.error("Null content for preset with source '" + myPreset.getSrc() + "' in flavor '" + flavorName 468 + "'"); 469 } else { 470 String cat = myPreset.getCategory(); 471 Map<String, String> allEntries; 472 if (presetsByCat.containsKey(cat)) { 473 allEntries = presetsByCat.get(cat); 474 } else { 475 allEntries = new HashMap<>(); 476 } 477 try { 478 Map<String, String> newEntries = PaletteParser.parse(content.getBytes(), myPreset.getSrc()); 479 if (newEntries != null) { 480 allEntries.putAll(newEntries); 481 } 482 if (allEntries.isEmpty()) { 483 presetsByCat.remove(cat); 484 } else { 485 presetsByCat.put(cat, allEntries); 486 } 487 } catch (PaletteParseException e) { 488 log.error( 489 String.format("Could not parse palette for " + "preset with source '%s' in flavor '%s'", 490 myPreset.getSrc(), flavorName), 491 e); 492 } 493 } 494 } 495 } 496 return presetsByCat; 497 } 498 499 @Override 500 public Map<String, String> getPresetVariables(String flavorName) { 501 Map<String, String> res = new HashMap<>(); 502 FlavorDescriptor flavor = getFlavor(flavorName); 503 if (flavor == null) { 504 return res; 505 } 506 Map<String, Map<String, String>> presetsByCat = getPresetsByCat(flavor); 507 for (String cat : presetsByCat.keySet()) { 508 Map<String, String> entries = presetsByCat.get(cat); 509 for (Map.Entry<String, String> entry : entries.entrySet()) { 510 res.put(entry.getKey() + " (" + ThemeStylingService.FLAVOR_MARKER + " " + cat + ")", entry.getValue()); 511 } 512 } 513 return res; 514 } 515 516 @Override 517 public PageDescriptor getPage(String name) { 518 PageDescriptor page = pageReg.getPage(name); 519 if (page != null) { 520 // merge with global resources 521 PageDescriptor globalPage = pageReg.getPage("*"); 522 mergePage(page, globalPage); 523 } 524 return page; 525 } 526 527 @Override 528 public List<PageDescriptor> getPages() { 529 List<PageDescriptor> pages = new ArrayList<>(); 530 List<String> names = pageReg.getPageNames(); 531 PageDescriptor globalPage = pageReg.getPage("*"); 532 for (String name : names) { 533 if ("*".equals(name)) { 534 continue; 535 } 536 PageDescriptor page = pageReg.getPage(name); 537 if (page != null) { 538 // merge with global resources 539 mergePage(page, globalPage); 540 } 541 pages.add(page); 542 } 543 return pages; 544 } 545 546 protected void mergePage(PageDescriptor page, PageDescriptor globalPage) { 547 if (page != null && globalPage != null) { 548 // merge with global resources 549 PageDescriptor clone = globalPage.clone(); 550 clone.setAppendFlavors(true); 551 clone.setAppendResources(true); 552 clone.setAppendStyles(true); 553 page.merge(clone); 554 } 555 } 556 557 @Override 558 public String negotiate(String target, Object context) { 559 String res = null; 560 NegotiationDescriptor negd = negReg.getNegotiation(target); 561 if (negd != null) { 562 List<NegotiatorDescriptor> nds = negd.getNegotiators(); 563 for (NegotiatorDescriptor nd : nds) { 564 Class<Negotiator> ndc = nd.getNegotiatorClass(); 565 try { 566 Negotiator neg = ndc.getDeclaredConstructor().newInstance(); 567 neg.setProperties(nd.getProperties()); 568 res = neg.getResult(target, context); 569 if (res != null) { 570 break; 571 } 572 } catch (ReflectiveOperationException e) { 573 throw new RuntimeException(e); 574 } 575 } 576 } 577 return res; 578 } 579 580}