/* * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Anahide Tchertchian */ package org.nuxeo.theme.styling.service; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.web.resources.api.Resource; import org.nuxeo.ecm.web.resources.api.ResourceType; import org.nuxeo.ecm.web.resources.api.service.WebResourceManager; import org.nuxeo.ecm.web.resources.core.ResourceDescriptor; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.logging.DeprecationLogger; import org.nuxeo.runtime.model.ComponentContext; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; import org.nuxeo.runtime.model.RuntimeContext; import org.nuxeo.theme.styling.negotiation.Negotiator; import org.nuxeo.theme.styling.service.descriptors.FlavorDescriptor; import org.nuxeo.theme.styling.service.descriptors.FlavorPresets; import org.nuxeo.theme.styling.service.descriptors.IconDescriptor; import org.nuxeo.theme.styling.service.descriptors.LogoDescriptor; import org.nuxeo.theme.styling.service.descriptors.NegotiationDescriptor; import org.nuxeo.theme.styling.service.descriptors.NegotiatorDescriptor; import org.nuxeo.theme.styling.service.descriptors.PageDescriptor; import org.nuxeo.theme.styling.service.descriptors.PalettePreview; import org.nuxeo.theme.styling.service.descriptors.SassImport; import org.nuxeo.theme.styling.service.descriptors.SimpleStyle; import org.nuxeo.theme.styling.service.palettes.PaletteParseException; import org.nuxeo.theme.styling.service.palettes.PaletteParser; import org.nuxeo.theme.styling.service.registries.FlavorRegistry; import org.nuxeo.theme.styling.service.registries.NegotiationRegistry; import org.nuxeo.theme.styling.service.registries.PageRegistry; /** * Default implementation for the {@link ThemeStylingService} * * @since 5.5 */ public class ThemeStylingServiceImpl extends DefaultComponent implements ThemeStylingService { private static final Log log = LogFactory.getLog(ThemeStylingServiceImpl.class); protected static final String WR_EX = "org.nuxeo.ecm.platform.WebResources"; protected PageRegistry pageReg; protected FlavorRegistry flavorReg; protected NegotiationRegistry negReg; // Runtime Component API @Override public void activate(ComponentContext context) { super.activate(context); pageReg = new PageRegistry(); flavorReg = new FlavorRegistry(); negReg = new NegotiationRegistry(); } @Override public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (contribution instanceof FlavorDescriptor) { FlavorDescriptor flavor = (FlavorDescriptor) contribution; log.info(String.format("Register flavor '%s'", flavor.getName())); registerFlavor(flavor, contributor.getContext()); log.info(String.format("Done registering flavor '%s'", flavor.getName())); } else if (contribution instanceof SimpleStyle) { SimpleStyle style = (SimpleStyle) contribution; log.info(String.format("Register style '%s'", style.getName())); String message = String.format("Style '%s' on component %s should now be contributed to extension " + "point '%s': a compatibility registration was performed but it may not be " + "accurate. Note that the 'flavor' processor should be used with this resource.", style.getName(), contributor.getName(), WR_EX); DeprecationLogger.log(message, "7.4"); Framework.getRuntime().getWarnings().add(message); ResourceDescriptor resource = getResourceFromStyle(style); registerResource(resource, contributor.getContext()); log.info(String.format("Done registering style '%s'", style.getName())); } else if (contribution instanceof PageDescriptor) { PageDescriptor page = (PageDescriptor) contribution; log.info(String.format("Register page '%s'", page.getName())); if (page.hasResources()) { // automatically register a bundle for page resources WebResourceManager wrm = Framework.getService(WebResourceManager.class); wrm.registerResourceBundle(page.getComputedResourceBundle()); } pageReg.addContribution(page); log.info(String.format("Done registering page '%s'", page.getName())); } else if (contribution instanceof ResourceDescriptor) { ResourceDescriptor resource = (ResourceDescriptor) contribution; log.info(String.format("Register resource '%s'", resource.getName())); String message = String.format("Resource '%s' on component %s should now be contributed to extension " + "point '%s': a compatibility registration was performed but it may not be accurate.", resource.getName(), contributor.getName(), WR_EX); DeprecationLogger.log(message, "7.4"); Framework.getRuntime().getWarnings().add(message); // ensure path is absolute, consider that resource is in the war, and if not, user will have to declare it // directly to the WRM endpoint String path = resource.getPath(); if (path != null && !path.startsWith("/")) { resource.setUri("/" + path); } registerResource(resource, contributor.getContext()); log.info(String.format("Done registering resource '%s'", resource.getName())); } else if (contribution instanceof NegotiationDescriptor) { NegotiationDescriptor neg = (NegotiationDescriptor) contribution; log.info(String.format("Register negotiation for '%s'", neg.getTarget())); negReg.addContribution(neg); log.info(String.format("Done registering negotiation for '%s'", neg.getTarget())); } else { log.error(String.format( "Unknown contribution to the theme " + "styling service, extension point '%s': '%s", extensionPoint, contribution)); } } @Override public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (contribution instanceof FlavorDescriptor) { FlavorDescriptor flavor = (FlavorDescriptor) contribution; flavorReg.removeContribution(flavor); } else if (contribution instanceof Resource) { Resource resource = (Resource) contribution; unregisterResource(resource); } else if (contribution instanceof SimpleStyle) { SimpleStyle style = (SimpleStyle) contribution; unregisterResource(getResourceFromStyle(style)); } else if (contribution instanceof PageDescriptor) { PageDescriptor page = (PageDescriptor) contribution; if (page.hasResources()) { WebResourceManager wrm = Framework.getService(WebResourceManager.class); wrm.unregisterResourceBundle(page.getComputedResourceBundle()); } pageReg.removeContribution(page); } else if (contribution instanceof NegotiationDescriptor) { NegotiationDescriptor neg = (NegotiationDescriptor) contribution; negReg.removeContribution(neg); } else { log.error(String.format( "Unknown contribution to the theme " + "styling service, extension point '%s': '%s", extensionPoint, contribution)); } } protected void registerFlavor(FlavorDescriptor flavor, RuntimeContext extensionContext) { // set flavor presets files content List<FlavorPresets> presets = flavor.getPresets(); if (presets != null) { for (FlavorPresets myPreset : presets) { String src = myPreset.getSrc(); URL url = getUrlFromPath(src, extensionContext); if (url == null) { log.error(String.format("Could not find resource at '%s'", src)); } else { String content; try { content = new String(IOUtils.toByteArray(url)); } catch (IOException e) { throw new RuntimeException(e); } myPreset.setContent(content); } } } // set flavor sass variables List<SassImport> sassVars = flavor.getSassImports(); if (sassVars != null) { for (SassImport var : sassVars) { String src = var.getSrc(); URL url = getUrlFromPath(src, extensionContext); if (url == null) { log.error(String.format("Could not find resource at '%s'", src)); } else { String content; try { content = new String(IOUtils.toByteArray(url)); } catch (IOException e) { throw new RuntimeException(e); } var.setContent(content); } } } flavorReg.addContribution(flavor); } protected List<FlavorPresets> computePresets(FlavorDescriptor flavor, List<String> flavors) { List<FlavorPresets> presets = new ArrayList<FlavorPresets>(); if (flavor != null) { List<FlavorPresets> localPresets = flavor.getPresets(); if (localPresets != null) { presets.addAll(localPresets); } String extendsFlavorName = flavor.getExtendsFlavor(); if (!StringUtils.isBlank(extendsFlavorName)) { if (flavors.contains(extendsFlavorName)) { // cyclic dependency => abort log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); return presets; } else { // retrieve the extended presets flavors.add(flavor.getName()); FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); if (extendedFlavor != null) { List<FlavorPresets> parentPresets = computePresets(extendedFlavor, flavors); if (parentPresets != null) { presets.addAll(0, parentPresets); } } else { log.warn("Extended flavor '" + extendsFlavorName + "' not found"); } } } } return presets; } protected void registerResource(Resource resource, RuntimeContext extensionContext) { WebResourceManager wrm = Framework.getService(WebResourceManager.class); wrm.registerResource(resource); } protected void unregisterResource(Resource resource) { // unregister directly to the WebResourceManager service WebResourceManager wrm = Framework.getService(WebResourceManager.class); wrm.unregisterResource(resource); } protected ResourceDescriptor getResourceFromStyle(SimpleStyle style) { // turn style into a resource ResourceDescriptor resource = new ResourceDescriptor(); resource.setPath(style.getSrc()); String name = style.getName(); if (name.endsWith(ResourceType.css.name())) { resource.setName(name); } else { resource.setName(name + "." + ResourceType.css.name()); } resource.setProcessors(Arrays.asList(new String[] { "flavor" })); return resource; } protected URL getUrlFromPath(String path, RuntimeContext extensionContext) { if (path == null) { return null; } URL url = null; try { url = new URL(path); } catch (MalformedURLException e) { url = extensionContext.getLocalResource(path); if (url == null) { url = extensionContext.getResource(path); } } return url; } // service API @Override public String getDefaultFlavorName(String themePageName) { if (pageReg != null) { PageDescriptor themePage = pageReg.getPage(themePageName); if (themePage != null) { return themePage.getDefaultFlavor(); } } return null; } @Override public FlavorDescriptor getFlavor(String flavorName) { if (flavorReg != null) { FlavorDescriptor flavor = flavorReg.getFlavor(flavorName); if (flavor != null) { FlavorDescriptor clone = flavor.clone(); clone.setLogo(computeLogo(flavor, new ArrayList<String>())); clone.setPalettePreview(computePalettePreview(flavor, new ArrayList<String>())); clone.setFavicons(computeIcons(flavor, new ArrayList<String>())); return clone; } } return null; } @Override public LogoDescriptor getLogo(String flavorName) { FlavorDescriptor flavor = getFlavor(flavorName); if (flavor != null) { return flavor.getLogo(); } return null; } protected LogoDescriptor computeLogo(FlavorDescriptor flavor, List<String> flavors) { if (flavor != null) { LogoDescriptor localLogo = flavor.getLogo(); if (localLogo == null) { String extendsFlavorName = flavor.getExtendsFlavor(); if (!StringUtils.isBlank(extendsFlavorName)) { if (flavors.contains(extendsFlavorName)) { // cyclic dependency => abort log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); return null; } else { // retrieved the extended logo flavors.add(flavor.getName()); FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); if (extendedFlavor != null) { localLogo = computeLogo(extendedFlavor, flavors); } else { log.warn("Extended flavor '" + extendsFlavorName + "' not found"); } } } } return localLogo; } return null; } protected PalettePreview computePalettePreview(FlavorDescriptor flavor, List<String> flavors) { if (flavor != null) { PalettePreview localPalette = flavor.getPalettePreview(); if (localPalette == null) { String extendsFlavorName = flavor.getExtendsFlavor(); if (!StringUtils.isBlank(extendsFlavorName)) { if (flavors.contains(extendsFlavorName)) { // cyclic dependency => abort log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); return null; } else { // retrieved the extended colors flavors.add(flavor.getName()); FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); if (extendedFlavor != null) { localPalette = computePalettePreview(extendedFlavor, flavors); } else { log.warn("Extended flavor '" + extendsFlavorName + "' not found"); } } } } return localPalette; } return null; } protected List<IconDescriptor> computeIcons(FlavorDescriptor flavor, List<String> flavors) { if (flavor != null) { List<IconDescriptor> localIcons = flavor.getFavicons(); if (localIcons == null || localIcons.isEmpty()) { String extendsFlavorName = flavor.getExtendsFlavor(); if (!StringUtils.isBlank(extendsFlavorName)) { if (flavors.contains(extendsFlavorName)) { // cyclic dependency => abort log.error("Cyclic dependency detected in flavor '" + flavor.getName() + "' hierarchy"); return null; } else { // retrieved the extended icons flavors.add(flavor.getName()); FlavorDescriptor extendedFlavor = getFlavor(extendsFlavorName); if (extendedFlavor != null) { localIcons = computeIcons(extendedFlavor, flavors); } else { log.warn("Extended flavor '" + extendsFlavorName + "' not found"); } } } } return localIcons; } return null; } @Override public List<String> getFlavorNames(String themePageName) { if (pageReg != null) { PageDescriptor themePage = pageReg.getPage(themePageName); if (themePage != null) { List<String> flavors = new ArrayList<String>(); List<String> localFlavors = themePage.getFlavors(); if (localFlavors != null) { flavors.addAll(localFlavors); } // add flavors from theme for all pages PageDescriptor forAllPage = pageReg.getConfigurationApplyingToAll(); if (forAllPage != null) { localFlavors = forAllPage.getFlavors(); if (localFlavors != null) { flavors.addAll(localFlavors); } } // add default flavor if it's not listed there String defaultFlavor = themePage.getDefaultFlavor(); if (defaultFlavor != null) { if (!flavors.contains(defaultFlavor)) { flavors.add(0, defaultFlavor); } } return flavors; } } return null; } @Override public List<FlavorDescriptor> getFlavors(String themePageName) { List<String> flavorNames = getFlavorNames(themePageName); if (flavorNames != null) { List<FlavorDescriptor> flavors = new ArrayList<FlavorDescriptor>(); for (String flavorName : flavorNames) { FlavorDescriptor flavor = getFlavor(flavorName); if (flavor != null) { flavors.add(flavor); } } return flavors; } return null; } protected Map<String, Map<String, String>> getPresetsByCat(FlavorDescriptor flavor) { String flavorName = flavor.getName(); List<FlavorPresets> presets = computePresets(flavor, new ArrayList<String>()); Map<String, Map<String, String>> presetsByCat = new HashMap<String, Map<String, String>>(); if (presets != null) { for (FlavorPresets myPreset : presets) { String content = myPreset.getContent(); if (content == null) { log.error("Null content for preset with source '" + myPreset.getSrc() + "' in flavor '" + flavorName + "'"); } else { String cat = myPreset.getCategory(); Map<String, String> allEntries; if (presetsByCat.containsKey(cat)) { allEntries = presetsByCat.get(cat); } else { allEntries = new HashMap<String, String>(); } try { Map<String, String> newEntries = PaletteParser.parse(content.getBytes(), myPreset.getSrc()); if (newEntries != null) { allEntries.putAll(newEntries); } if (allEntries.isEmpty()) { presetsByCat.remove(cat); } else { presetsByCat.put(cat, allEntries); } } catch (PaletteParseException e) { log.error(String.format("Could not parse palette for " + "preset with source '%s' in flavor '%s'", myPreset.getSrc(), flavorName), e); } } } } return presetsByCat; } @Override public Map<String, String> getPresetVariables(String flavorName) { Map<String, String> res = new HashMap<String, String>(); FlavorDescriptor flavor = getFlavor(flavorName); if (flavor == null) { return res; } Map<String, Map<String, String>> presetsByCat = getPresetsByCat(flavor); for (String cat : presetsByCat.keySet()) { Map<String, String> entries = presetsByCat.get(cat); for (Map.Entry<String, String> entry : entries.entrySet()) { res.put(entry.getKey() + " (" + ThemeStylingService.FLAVOR_MARKER + " " + cat + ")", entry.getValue()); } } return res; } @Override public PageDescriptor getPage(String name) { PageDescriptor page = pageReg.getPage(name); if (page != null) { // merge with global resources PageDescriptor globalPage = pageReg.getPage("*"); mergePage(page, globalPage); } return page; } @Override public List<PageDescriptor> getPages() { List<PageDescriptor> pages = new ArrayList<PageDescriptor>(); List<String> names = pageReg.getPageNames(); PageDescriptor globalPage = pageReg.getPage("*"); for (String name : names) { if ("*".equals(name)) { continue; } PageDescriptor page = pageReg.getPage(name); if (page != null) { // merge with global resources mergePage(page, globalPage); } pages.add(page); } return pages; } protected void mergePage(PageDescriptor page, PageDescriptor globalPage) { if (page != null && globalPage != null) { // merge with global resources PageDescriptor clone = globalPage.clone(); clone.setAppendFlavors(true); clone.setAppendResources(true); clone.setAppendStyles(true); page.merge(clone); } } @Override public String negotiate(String target, Object context) { String res = null; NegotiationDescriptor negd = negReg.getNegotiation(target); if (negd != null) { List<NegotiatorDescriptor> nds = negd.getNegotiators(); for (NegotiatorDescriptor nd : nds) { Class<Negotiator> ndc = nd.getNegotiatorClass(); try { Negotiator neg = ndc.newInstance(); neg.setProperties(nd.getProperties()); res = neg.getResult(target, context); if (res != null) { break; } } catch (IllegalAccessException | InstantiationException e) { throw new RuntimeException(e); } } } return res; } }