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