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}