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