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