001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS <http://nuxeo.com> and others
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Jean-Marc Orliaguet, Chalmers
011 *
012 * $Id$
013 */
014
015package org.nuxeo.theme.themes;
016
017import java.io.BufferedReader;
018import java.io.File;
019import java.io.FilenameFilter;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.Reader;
024import java.net.MalformedURLException;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.Iterator;
033import java.util.LinkedHashMap;
034import java.util.LinkedHashSet;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.regex.Matcher;
039import java.util.regex.Pattern;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.codehaus.plexus.util.dag.CycleDetectedException;
044import org.codehaus.plexus.util.dag.DAG;
045import org.codehaus.plexus.util.dag.TopologicalSorter;
046import org.nuxeo.common.Environment;
047import org.nuxeo.runtime.api.Framework;
048import org.nuxeo.runtime.services.event.Event;
049import org.nuxeo.runtime.services.event.EventService;
050import org.nuxeo.theme.ApplicationType;
051import org.nuxeo.theme.CustomThemeNameFilter;
052import org.nuxeo.theme.Manager;
053import org.nuxeo.theme.NegotiationDef;
054import org.nuxeo.theme.Registrable;
055import org.nuxeo.theme.Utils;
056import org.nuxeo.theme.elements.Element;
057import org.nuxeo.theme.elements.ElementFactory;
058import org.nuxeo.theme.elements.ElementFormatter;
059import org.nuxeo.theme.elements.ElementType;
060import org.nuxeo.theme.elements.PageElement;
061import org.nuxeo.theme.elements.ThemeElement;
062import org.nuxeo.theme.engines.EngineType;
063import org.nuxeo.theme.formats.Format;
064import org.nuxeo.theme.formats.FormatFactory;
065import org.nuxeo.theme.formats.FormatType;
066import org.nuxeo.theme.formats.layouts.Layout;
067import org.nuxeo.theme.formats.styles.Style;
068import org.nuxeo.theme.formats.widgets.Widget;
069import org.nuxeo.theme.fragments.Fragment;
070import org.nuxeo.theme.fragments.FragmentFactory;
071import org.nuxeo.theme.fragments.FragmentType;
072import org.nuxeo.theme.models.Info;
073import org.nuxeo.theme.models.ModelType;
074import org.nuxeo.theme.nodes.Node;
075import org.nuxeo.theme.nodes.NodeException;
076import org.nuxeo.theme.perspectives.PerspectiveManager;
077import org.nuxeo.theme.perspectives.PerspectiveType;
078import org.nuxeo.theme.properties.FieldIO;
079import org.nuxeo.theme.relations.DefaultPredicate;
080import org.nuxeo.theme.relations.DyadicRelation;
081import org.nuxeo.theme.relations.Predicate;
082import org.nuxeo.theme.relations.Relation;
083import org.nuxeo.theme.resources.ResourceBank;
084import org.nuxeo.theme.resources.ResourceManager;
085import org.nuxeo.theme.resources.ResourceType;
086import org.nuxeo.theme.templates.TemplateEngineType;
087import org.nuxeo.theme.types.Type;
088import org.nuxeo.theme.types.TypeFamily;
089import org.nuxeo.theme.types.TypeRegistry;
090import org.nuxeo.theme.uids.Identifiable;
091import org.nuxeo.theme.uids.UidManager;
092import org.nuxeo.theme.views.ViewType;
093
094public final class ThemeManager implements Registrable {
095
096    public static final String THEME_TOPIC = "org.nuxeo.theme";
097
098    public static final String THEME_REGISTERED_EVENT_ID = "themeRegistered";
099
100    private static final Log log = LogFactory.getLog(ThemeManager.class);
101
102    private final Map<String, Long> lastModified = new HashMap<String, Long>();
103
104    private final Map<String, ThemeElement> themes = new HashMap<String, ThemeElement>();
105
106    private final Map<String, PageElement> pages = new HashMap<String, PageElement>();
107
108    private final Map<String, List<Integer>> formatsByTypeName = new LinkedHashMap<String, List<Integer>>();
109
110    private final Map<String, ModelType> modelsByClassname = new HashMap<String, ModelType>();
111
112    private final Map<String, Map<String, Integer>> namedObjectsByTheme = new HashMap<String, Map<String, Integer>>();
113
114    private final Map<Integer, String> themeOfNamedObjects = new HashMap<Integer, String>();
115
116    private final Map<String, Info> infoMap = new HashMap<String, Info>();
117
118    private static final Predicate PREDICATE_FORMAT_INHERIT = new DefaultPredicate("_ inherits from _");
119
120    private final Map<String, String> cachedStyles = new HashMap<String, String>();
121
122    private final Map<String, String> cachedResources = new HashMap<String, String>();
123
124    private final Map<String, byte[]> cachedBinaries = new HashMap<String, byte[]>();
125
126    private final List<String> resourceOrdering = new ArrayList<String>();
127
128    private static File THEME_DIR;
129
130    private static final FilenameFilter CUSTOM_THEME_FILENAME_FILTER = new CustomThemeNameFilter();
131
132    private static final int DEFAULT_THEME_INDENT = 2;
133
134    private static final String COLLECTION_CSS_MARKER = "COLLECTION";
135
136    private static final Pattern styleResourceNamePattern = Pattern.compile("(.*?)\\s\\((.*?)\\)$", Pattern.DOTALL);
137
138    public static void createThemeDir() {
139        THEME_DIR = new File(Environment.getDefault().getData(), "themes");
140        THEME_DIR.mkdirs();
141    }
142
143    public static File getThemeDir() {
144        if (THEME_DIR == null || !THEME_DIR.exists()) {
145            createThemeDir();
146        }
147        return THEME_DIR;
148    }
149
150    @Override
151    public synchronized void clear() {
152        themes.clear();
153        pages.clear();
154        formatsByTypeName.clear();
155        modelsByClassname.clear();
156        namedObjectsByTheme.clear();
157        themeOfNamedObjects.clear();
158        infoMap.clear();
159        cachedStyles.clear();
160        cachedResources.clear();
161        cachedBinaries.clear();
162        resourceOrdering.clear();
163        lastModified.clear();
164    }
165
166    public Map<String, Info> getGlobalInfoMap() {
167        return infoMap;
168    }
169
170    public static boolean validateThemeName(String themeName) {
171        return (themeName.matches("^([a-zA-Z]|[a-zA-Z][a-zA-Z0-9_\\-]*?[a-zA-Z0-9])$"));
172    }
173
174    public static String getCustomThemePath(String themeName) throws ThemeIOException {
175        String themeFileName = String.format("theme-%s.xml", themeName);
176        File file = new File(getThemeDir(), themeFileName);
177        try {
178            return file.getCanonicalPath();
179        } catch (IOException e) {
180            throw new ThemeIOException("Could not get custom theme path: " + themeName, e);
181        }
182    }
183
184    public static List<File> getCustomThemeFiles() {
185        List<File> files = new ArrayList<File>();
186        for (File f : getThemeDir().listFiles(CUSTOM_THEME_FILENAME_FILTER)) {
187            files.add(f);
188        }
189        return files;
190    }
191
192    public static ThemeDescriptor customizeTheme(ThemeDescriptor themeDescriptor) throws ThemeException {
193        String themeName = themeDescriptor.getName();
194        if (!themeDescriptor.isCustomizable()) {
195            throw new ThemeException("Theme : " + themeName + " cannot be customized.");
196        }
197
198        ThemeSerializer serializer = new ThemeSerializer();
199        String xmlSource;
200        try {
201            xmlSource = serializer.serializeToXml(themeDescriptor.getSrc(), 0);
202        } catch (ThemeIOException e) {
203            throw new ThemeException("Could not serialize theme: " + themeName, e);
204        }
205        ThemeDescriptor newThemeDescriptor = createCustomTheme(themeName);
206        String newSrc = newThemeDescriptor.getSrc();
207        try {
208            Manager.getThemeManager().loadTheme(newSrc, xmlSource);
209        } catch (ThemeIOException e) {
210            throw new ThemeException("Could not update theme: " + newSrc, e);
211        }
212        try {
213            saveTheme(newSrc);
214        } catch (ThemeIOException e) {
215            throw new ThemeException("Could not save theme: " + newSrc, e);
216        }
217
218        newThemeDescriptor.setCustomization(true);
219        return newThemeDescriptor;
220    }
221
222    public static ThemeDescriptor uncustomizeTheme(ThemeDescriptor themeDescriptor) throws ThemeException {
223        ThemeManager themeManager = Manager.getThemeManager();
224        String themeName = themeDescriptor.getName();
225
226        if (!themeDescriptor.isCustomization()) {
227            throw new ThemeException("Theme : " + themeName + " cannot be uncustomized.");
228        }
229
230        String themeSrc = themeDescriptor.getSrc();
231        try {
232            themeManager.deleteTheme(themeSrc);
233        } catch (ThemeIOException e) {
234            throw new ThemeException("Could not remove theme: " + themeSrc, e);
235        }
236
237        ThemeDescriptor newThemeDescriptor = getThemeDescriptorByThemeName(themeName);
238        loadTheme(newThemeDescriptor);
239        return newThemeDescriptor;
240    }
241
242    public static ThemeDescriptor createCustomTheme(String name) throws ThemeException {
243        ThemeManager themeManager = Manager.getThemeManager();
244        ThemeElement theme = (ThemeElement) ElementFactory.create("theme");
245        theme.setName(name);
246        Format themeWidget = themeManager.createWidget();
247        themeWidget.setName("theme view");
248        ElementFormatter.setFormat(theme, themeWidget);
249        // default page
250        PageElement page = (PageElement) ElementFactory.create("page");
251        page.setName("default");
252        Format pageWidget = themeManager.createWidget();
253        pageWidget.setName("page frame");
254        Format pageLayout = themeManager.createLayout();
255        Format pageStyle = themeManager.createStyle();
256        ElementFormatter.setFormat(page, pageWidget);
257        ElementFormatter.setFormat(page, pageStyle);
258        ElementFormatter.setFormat(page, pageLayout);
259        try {
260            theme.addChild(page);
261        } catch (NodeException e) {
262            throw new ThemeException(e.getMessage(), e);
263        }
264        // create a theme descriptor
265        ThemeDescriptor themeDescriptor = new ThemeDescriptor();
266        themeDescriptor.setName(name);
267        String path;
268        try {
269            path = ThemeManager.getCustomThemePath(name);
270        } catch (ThemeIOException e) {
271            throw new ThemeException("Could not get file path for theme: " + name);
272        }
273        final String src = String.format("file:///%s", path);
274        themeDescriptor.setSrc(src);
275        TypeRegistry typeRegistry = Manager.getTypeRegistry();
276        typeRegistry.register(themeDescriptor);
277        // register the theme
278        themeManager.registerTheme(theme);
279        // save the theme
280        try {
281            ThemeManager.saveTheme(themeDescriptor.getSrc());
282        } catch (ThemeIOException e) {
283            throw new ThemeException("Could not save theme: " + name, e);
284        }
285        return themeDescriptor;
286    }
287
288    public static void updateThemeDescriptors() {
289        Map<String, List<ThemeDescriptor>> names = new HashMap<String, List<ThemeDescriptor>>();
290        for (ThemeDescriptor themeDescriptor : getThemeDescriptors()) {
291            String themeName = themeDescriptor.getName();
292            if (!names.containsKey(themeName)) {
293                names.put(themeName, new ArrayList<ThemeDescriptor>());
294            }
295            names.get(themeName).add(themeDescriptor);
296        }
297        for (List<ThemeDescriptor> themeDescriptors : names.values()) {
298            for (ThemeDescriptor themeDescriptor : themeDescriptors) {
299                themeDescriptor.setCustomized(true);
300                themeDescriptor.setCustomization(false);
301            }
302            int size = themeDescriptors.size();
303            themeDescriptors.get(size - 1).setCustomized(false);
304            if (size > 1) {
305                themeDescriptors.get(size - 1).setCustomization(true);
306            }
307        }
308    }
309
310    public static String getDefaultTheme(final String applicationPath) {
311        return getDefaultTheme(applicationPath, null);
312    }
313
314    public static String getDefaultTheme(final String... paths) {
315        String defaultTheme = "";
316        ApplicationType application = null;
317        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
318        application = (ApplicationType) typeRegistry.lookup(TypeFamily.APPLICATION, paths);
319        if (application != null) {
320            NegotiationDef negotiation = application.getNegotiation();
321            if (negotiation != null) {
322                defaultTheme = negotiation.getDefaultTheme();
323            }
324        }
325        return defaultTheme;
326    }
327
328    public static Set<String> getThemeNames(final String templateEngine) {
329        Set<String> names = new HashSet<String>();
330        for (ThemeDescriptor themeDef : getThemeDescriptors()) {
331            // Skip customized themes
332            if (themeDef.isCustomized()) {
333                continue;
334            }
335            if (templateEngine != null && !themeDef.isCompatibleWith(templateEngine)) {
336                continue;
337            }
338            names.add(themeDef.getName());
339        }
340        return names;
341    }
342
343    public static ThemeDescriptor getThemeDescriptorByThemeName(final String templateEngine, final String themeName) {
344        for (ThemeDescriptor themeDef : getThemeDescriptors()) {
345            // Skip customized themes
346            if (themeDef.isCustomized()) {
347                continue;
348            }
349            if (templateEngine != null && !themeDef.isCompatibleWith(templateEngine)) {
350                continue;
351            }
352            final String name = themeDef.getName();
353            if (name != null && name.equals(themeName)) {
354                return themeDef;
355            }
356        }
357        return null;
358    }
359
360    public static ThemeDescriptor getThemeDescriptorByThemeName(final String themeName) {
361        return getThemeDescriptorByThemeName(null, themeName);
362    }
363
364    public static Set<String> getThemeNames() {
365        return getThemeNames(null);
366    }
367
368    public Set<String> getPageNames(final String themeName) {
369        final ThemeElement theme = getThemeByName(themeName);
370        final Set<String> pageNames = new LinkedHashSet<String>();
371        if (theme != null) {
372            for (PageElement page : getPagesOf(theme)) {
373                pageNames.add(page.getName());
374            }
375        }
376        return pageNames;
377    }
378
379    public static List<PageElement> getPagesOf(final ThemeElement theme) {
380        final List<PageElement> themePages = new ArrayList<PageElement>();
381        for (Node node : theme.getChildren()) {
382            final PageElement page = (PageElement) node;
383            themePages.add(page);
384        }
385        return themePages;
386    }
387
388    public List<PageElement> getPagesOf(final String themeName) {
389        final ThemeElement theme = getThemeByName(themeName);
390        if (theme == null) {
391            return null;
392        }
393        return getPagesOf(theme);
394    }
395
396    public static ThemeElement getThemeOf(final Element element) {
397        ThemeElement theme = null;
398        Element current = element;
399        while (current != null) {
400            if (current instanceof ThemeElement) {
401                theme = (ThemeElement) current;
402                break;
403            }
404            current = (Element) current.getParent();
405        }
406        return theme;
407    }
408
409    public static boolean belongToSameTheme(final Element element1, final Element element2) {
410        return getThemeOf(element1) == getThemeOf(element2);
411    }
412
413    // Object lookups by URL
414    public static EngineType getEngineByUrl(final URL url) {
415        if (url == null) {
416            return null;
417        }
418        final String[] path = url.getPath().split("/");
419        if (path.length <= 1) {
420            return null;
421        }
422        final String engineName = path[1];
423        return (EngineType) Manager.getTypeRegistry().lookup(TypeFamily.ENGINE, engineName);
424    }
425
426    public static String getViewModeByUrl(final URL url) {
427        if (url == null) {
428            return null;
429        }
430        final String[] path = url.getPath().split("/");
431        if (path.length <= 2) {
432            return null;
433        }
434        return path[2];
435    }
436
437    public static TemplateEngineType getTemplateEngineByUrl(final URL url) {
438        if (url == null) {
439            return null;
440        }
441        final String[] path = url.getPath().split("/");
442        if (path.length <= 3) {
443            return null;
444        }
445        final String templateEngineName = path[3];
446        return (TemplateEngineType) Manager.getTypeRegistry().lookup(TypeFamily.TEMPLATE_ENGINE, templateEngineName);
447    }
448
449    public ThemeElement getThemeBySrc(final String src) throws ThemeException {
450        ThemeDescriptor themeDef = getThemeDescriptor(src);
451        if (themeDef.isCustomized()) {
452            throw new ThemeException("Cannot access customized theme: " + src);
453        }
454        String themeName = themeDef.getName();
455        return getThemeByName(themeName);
456    }
457
458    public ThemeElement getThemeByUrl(final URL url) {
459        String themeName = getThemeNameByUrl(url);
460        if (themeName == null) {
461            return null;
462        }
463        return getThemeByName(themeName);
464    }
465
466    public static String getThemeNameByUrl(final URL url) {
467        if (url == null) {
468            return null;
469        }
470        if (!url.getHost().equals("theme")) {
471            return null;
472        }
473        final String[] path = url.getPath().split("/");
474        if (path.length <= 4) {
475            return null;
476        }
477        return path[4];
478    }
479
480    public static String getPagePathByUrl(final URL url) {
481        if (url == null) {
482            return null;
483        }
484        if (!url.getHost().equals("theme")) {
485            return null;
486        }
487        final String[] path = url.getPath().split("/");
488        if (path.length <= 5) {
489            return null;
490        }
491        final String pagePath = path[4] + '/' + path[5];
492        return pagePath;
493    }
494
495    public PageElement getThemePageByUrl(final URL url) {
496        if (url == null) {
497            return null;
498        }
499        if (!url.getHost().equals("theme")) {
500            return null;
501        }
502        final String pagePath = getPagePathByUrl(url);
503        return getPageByPath(pagePath);
504    }
505
506    public PageElement getPageByPath(final String path) {
507        return pages.get(path);
508    }
509
510    public static String getPageNameFromPagePath(final String path) {
511        if (path.contains("/")) {
512            return path.split("/")[1];
513        }
514        return null;
515    }
516
517    public ThemeElement getThemeByName(final String name) {
518        return themes.get(name);
519    }
520
521    public void fillScratchPage(final String themeName, final Element element) throws NodeException, ThemeException {
522        String pagePath = String.format("%s/~", themeName);
523
524        PageElement scratchPage = getPageByPath(pagePath);
525        if (scratchPage != null) {
526            destroyDescendants(scratchPage);
527            removeRelationsOf(scratchPage);
528            pages.remove(pagePath);
529            removeOrphanedFormats();
530        }
531
532        // create a new scratch page
533        scratchPage = (PageElement) ElementFactory.create("page");
534        Widget pageWidget = (Widget) FormatFactory.create("widget");
535        pageWidget.setName("page frame");
536        registerFormat(pageWidget);
537        ElementFormatter.setFormat(scratchPage, pageWidget);
538
539        UidManager uidManager = Manager.getUidManager();
540        uidManager.register(scratchPage);
541        pages.put(pagePath, scratchPage);
542
543        scratchPage.addChild(element);
544    }
545
546    public static Element getElementByUrl(final URL url) {
547        if (url == null) {
548            return null;
549        }
550        if (!url.getHost().equals("element")) {
551            return null;
552        }
553        final String[] path = url.getPath().split("/");
554        if (path.length < 1) {
555            return null;
556        }
557        final String uid = path[path.length - 1];
558        return (Element) Manager.getUidManager().getObjectByUid(Integer.valueOf(uid));
559    }
560
561    public static PerspectiveType getPerspectiveByUrl(final URL url) {
562        if (url == null) {
563            return null;
564        }
565        if (!url.getHost().equals("theme")) {
566            return null;
567        }
568        final String[] path = url.getPath().split("/");
569        if (path.length <= 6) {
570            return null;
571        }
572        final String perspectiveName = path[6];
573        return (PerspectiveType) Manager.getTypeRegistry().lookup(TypeFamily.PERSPECTIVE, perspectiveName);
574    }
575
576    public static String getCollectionNameByUrl(final URL url) {
577        if (url == null) {
578            return null;
579        }
580        if (!url.getHost().equals("theme")) {
581            return null;
582        }
583        final String[] path = url.getPath().split("/");
584        if (path.length <= 7) {
585            return null;
586        }
587        final String collectionName = path[7];
588        // TODO: check to see if the collection exists?
589        return collectionName;
590    }
591
592    public static String getUrlDescription(URL url) {
593        final String[] path = url.getPath().split("/");
594        String host = url.getHost();
595        String description = "[???]";
596        if ("theme".equals(host)) {
597            description = String.format("[THEME %s, PAGE %s, ENGINE %s, TEMPLATE %s, PERSPECTIVE %s, MODE %s]",
598                    path[4], path[5], path[1], path[3], path[6], path[2]);
599        } else if ("element".equals(host)) {
600            description = String.format("[ELEMENT %s, ENGINE %s, TEMPLATE %s, MODE %s]", path[4], path[1], path[3],
601                    path[2]);
602        }
603        return description;
604    }
605
606    // Named objects
607    public Identifiable getNamedObject(final String themeName, final String realm, final String name) {
608        final Map<String, Integer> objectsInTheme = namedObjectsByTheme.get(themeName);
609        if (objectsInTheme == null) {
610            return null;
611        }
612        final Integer uid = objectsInTheme.get(String.format("%s/%s", realm, name));
613
614        if (uid != null) {
615            return (Identifiable) Manager.getUidManager().getObjectByUid(uid);
616        }
617        return null;
618    }
619
620    public String getThemeNameOfNamedObject(Identifiable object) {
621        return themeOfNamedObjects.get(object.getUid());
622    }
623
624    public void setNamedObject(final String themeName, final String realm, final Identifiable object)
625            throws ThemeException {
626        if (!namedObjectsByTheme.containsKey(themeName)) {
627            namedObjectsByTheme.put(themeName, new LinkedHashMap<String, Integer>());
628        }
629        final Integer uid = object.getUid();
630        final String name = object.getName();
631        if (name == null) {
632            throw new ThemeException("Cannot register unnamed object, uid: " + uid);
633        }
634        namedObjectsByTheme.get(themeName).put(String.format("%s/%s", realm, name), uid);
635        themeOfNamedObjects.put(uid, themeName);
636    }
637
638    public List<Identifiable> getNamedObjects(final String themeName, final String realm) {
639        final List<Identifiable> objects = new ArrayList<Identifiable>();
640        final Map<String, Integer> objectsInTheme = namedObjectsByTheme.get(themeName);
641        final String prefix = String.format("%s/", realm);
642        final UidManager uidManager = Manager.getUidManager();
643        if (objectsInTheme != null) {
644            for (Map.Entry<String, Integer> entry : objectsInTheme.entrySet()) {
645                if (entry.getKey().startsWith(prefix)) {
646                    final Identifiable object = (Identifiable) uidManager.getObjectByUid(entry.getValue());
647                    objects.add(object);
648                }
649            }
650        }
651        return objects;
652    }
653
654    public void removeNamedObject(final String themeName, final String realm, final String name) {
655        final String key = String.format("%s/%s", realm, name);
656        Identifiable object = getNamedObject(themeName, realm, name);
657        themeOfNamedObjects.remove(object.getUid());
658        namedObjectsByTheme.get(themeName).remove(key);
659    }
660
661    public void removeNamedObjects(final String themeName) {
662        namedObjectsByTheme.remove(themeName);
663        List<Integer> toDelete = new ArrayList<Integer>();
664        for (Map.Entry<Integer, String> entry : themeOfNamedObjects.entrySet()) {
665            if (entry.getValue().equals(themeName)) {
666                toDelete.add(entry.getKey());
667            }
668        }
669        for (Integer key : toDelete) {
670            themeOfNamedObjects.remove(key);
671        }
672        toDelete = null;
673    }
674
675    public void makeElementUseNamedStyle(final Element element, final String inheritedName, final String themeName)
676            throws ThemeException {
677        final FormatType styleType = (FormatType) Manager.getTypeRegistry().lookup(TypeFamily.FORMAT, "style");
678        Style style = (Style) ElementFormatter.getFormatByType(element, styleType);
679
680        if (style == null) {
681            throw new ThemeException("Element has no assigned style: " + element.computeXPath());
682        }
683        // Make the style no longer inherits from other another style if
684        // 'inheritedName' is null
685        if (inheritedName == null) {
686            ThemeManager.removeInheritanceTowards(style);
687        } else {
688            Style inheritedStyle = (Style) getNamedObject(themeName, "style", inheritedName);
689            if (inheritedStyle == null) {
690                throw new ThemeException("Could not find named style: " + inheritedName);
691            }
692            makeFormatInherit(style, inheritedStyle);
693        }
694    }
695
696    public static void setStyleInheritance(String styleName, String ancestorStyleName, String themeName,
697            boolean allowMany) throws ThemeException {
698
699        ThemeManager themeManager = Manager.getThemeManager();
700        ThemeDescriptor themeDescriptor = ThemeManager.getThemeDescriptorByThemeName(themeName);
701        if (themeDescriptor == null) {
702            throw new ThemeException("Theme not found: " + themeName);
703        }
704        Style style = (Style) themeManager.getNamedObject(themeName, "style", styleName);
705        if (style == null) {
706            throw new ThemeException("Could not find named style: " + styleName);
707        }
708
709        Style ancestorStyle = (Style) themeManager.getNamedObject(themeName, "style", ancestorStyleName);
710        if (ancestorStyle == null) {
711            throw new ThemeException("Could not find named style: " + ancestorStyleName);
712        }
713        if (!allowMany) {
714            ThemeManager.removeInheritanceFrom(ancestorStyle);
715        }
716        themeManager.makeFormatInherit(style, ancestorStyle);
717    }
718
719    public static void loadRemoteStyle(String resourceBankName, Style style) throws ThemeException {
720        if (!style.isNamed()) {
721            throw new ThemeException("Only named styles can be loaded from resource banks.");
722        }
723        String styleName = style.getName();
724        final Matcher resourceNameMatcher = styleResourceNamePattern.matcher(styleName);
725        if (resourceNameMatcher.find()) {
726            String collectionName = resourceNameMatcher.group(2);
727            String resourceId = resourceNameMatcher.group(1) + ".css";
728            String cssSource = ResourceManager.getBankResource(resourceBankName, collectionName, "style", resourceId);
729            style.setCollection(collectionName);
730            Utils.loadCss(style, cssSource, "*");
731        } else {
732            throw new ThemeException("Incorrect remote style name: " + styleName);
733        }
734    }
735
736    // Element actions
737    public Element duplicateElement(final Element element, final boolean duplicateFormats) throws ThemeException {
738        Element duplicate;
739        final String typeName = element.getElementType().getTypeName();
740
741        if (element instanceof Fragment) {
742            final FragmentType fragmentType = ((Fragment) element).getFragmentType();
743            duplicate = FragmentFactory.create(fragmentType.getTypeName());
744        } else {
745            duplicate = ElementFactory.create(typeName);
746        }
747
748        if (duplicate == null) {
749            log.warn("Could not duplicate: " + element);
750        } else {
751            // duplicate the fields
752            try {
753                FieldIO.updateFieldsFromProperties(duplicate, FieldIO.dumpFieldsToProperties(element));
754            } catch (ThemeIOException e) {
755                log.warn("Could not copy the fields of: " + element);
756                log.debug(e.getMessage(), e);
757            }
758
759            // duplicate formats or create a relation
760            for (Format format : ElementFormatter.getFormatsFor(element)) {
761                if (duplicateFormats) {
762                    format = duplicateFormat(format);
763                }
764                ElementFormatter.setFormat(duplicate, format);
765            }
766
767            // duplicate description
768            duplicate.setDescription(element.getDescription());
769
770            // duplicate visibility
771            PerspectiveManager perspectiveManager = Manager.getPerspectiveManager();
772            for (PerspectiveType perspective : perspectiveManager.getPerspectivesFor(element)) {
773                PerspectiveManager.setVisibleInPerspective(duplicate, perspective);
774            }
775        }
776        return duplicate;
777    }
778
779    public void destroyElement(final Element element) throws ThemeException, NodeException {
780        final Element parent = (Element) element.getParent();
781
782        if (element instanceof ThemeElement) {
783            removeNamedStylesOf(element.getName());
784            unregisterTheme((ThemeElement) element);
785            destroyDescendants(element);
786            removeRelationsOf(element);
787
788        } else if (element instanceof PageElement) {
789            unregisterPage((PageElement) element);
790            destroyDescendants(element);
791            removeRelationsOf(element);
792            if (parent != null) {
793                parent.removeChild(element);
794            }
795
796        } else {
797            destroyDescendants(element);
798            removeRelationsOf(element);
799            if (parent != null) {
800                parent.removeChild(element);
801            }
802        }
803
804        // Final cleanup: remove formats that are not used by any element.
805        removeOrphanedFormats();
806    }
807
808    public void removeNamedStylesOf(String themeName) throws ThemeException {
809        ThemeManager themeManager = Manager.getThemeManager();
810        final UidManager uidManager = Manager.getUidManager();
811        for (Style style : themeManager.getNamedStyles(themeName)) {
812            removeNamedObject(themeName, "style", style.getName());
813            deleteFormat(style);
814            uidManager.unregister(style);
815        }
816    }
817
818    // Formats
819    public Format duplicateFormat(final Format format) throws ThemeException {
820        final String typeName = format.getFormatType().getTypeName();
821        final Format duplicate = FormatFactory.create(typeName);
822        registerFormat(duplicate);
823
824        duplicate.setName(format.getName());
825        duplicate.setDescription(format.getDescription());
826        duplicate.clonePropertiesOf(format);
827
828        final Format ancestor = getAncestorFormatOf(format);
829        if (ancestor != null) {
830            makeFormatInherit(duplicate, ancestor);
831        }
832        return duplicate;
833    }
834
835    public List<Format> listFormats() {
836        final UidManager uidManager = Manager.getUidManager();
837        List<Format> formats = new ArrayList<Format>();
838        for (Map.Entry<String, List<Integer>> entry : formatsByTypeName.entrySet()) {
839            for (Integer uid : entry.getValue()) {
840                Format format = (Format) uidManager.getObjectByUid(uid);
841                formats.add(format);
842            }
843        }
844        return formats;
845    }
846
847    public void registerFormat(final Format format) throws ThemeException {
848        final Integer id = format.getUid();
849        if (id == null) {
850            throw new ThemeException("Cannot register a format without an id");
851        }
852        final String formatTypeName = format.getFormatType().getTypeName();
853        if (formatTypeName == null) {
854            throw new ThemeException("Cannot register a format without a type");
855        }
856        if (!formatsByTypeName.containsKey(formatTypeName)) {
857            formatsByTypeName.put(formatTypeName, new ArrayList<Integer>());
858        }
859        final List<Integer> ids = formatsByTypeName.get(formatTypeName);
860        if (ids.contains(id)) {
861            throw new ThemeException("Cannot register a format twice: " + id);
862        }
863        ids.add(id);
864    }
865
866    public void unregisterFormat(final Format format) throws ThemeException {
867        final Integer id = format.getUid();
868        if (id == null) {
869            throw new ThemeException("Cannot unregister a format without an id");
870        }
871        final String formatTypeName = format.getFormatType().getTypeName();
872        if (formatTypeName == null) {
873            throw new ThemeException("Cannot unregister a format without a type");
874        }
875        if (formatsByTypeName.containsKey(formatTypeName)) {
876            final List<Integer> ids = formatsByTypeName.get(formatTypeName);
877            if (!ids.contains(id)) {
878                throw new ThemeException("Format with id: " + id + " is not registered.");
879            }
880            ids.remove(id);
881        }
882        removeInheritanceTowards(format);
883        removeInheritanceFrom(format);
884    }
885
886    public Set<String> getFormatTypeNames() {
887        return new LinkedHashSet<String>(formatsByTypeName.keySet());
888    }
889
890    public List<Format> getFormatsByTypeName(final String formatTypeName) {
891        List<Format> formats = new ArrayList<Format>();
892        if (!formatsByTypeName.containsKey(formatTypeName)) {
893            return formats;
894        }
895        UidManager uidManager = Manager.getUidManager();
896        for (Integer id : formatsByTypeName.get(formatTypeName)) {
897            formats.add((Format) uidManager.getObjectByUid(id));
898        }
899        return formats;
900    }
901
902    public List<Style> getStyles() {
903        return getStyles(null);
904    }
905
906    public List<Style> getStyles(String themeName) {
907        List<Style> styles = new ArrayList<Style>();
908        for (Format format : getFormatsByTypeName("style")) {
909            Style style = (Style) format;
910            if (themeName != null) {
911                ThemeElement theme = getThemeOfFormat(style);
912                if (theme == null) {
913                    if (!style.isNamed()) {
914                        log.warn("THEME inconsistency: " + style + " is not associated to any element.");
915                    }
916                    continue;
917                }
918                if (!themeName.equals(theme.getName())) {
919                    continue;
920                }
921            }
922            styles.add(style);
923        }
924        return styles;
925    }
926
927    public List<Style> getNamedStyles(String themeName) {
928        List<Style> styles = new ArrayList<Style>();
929        // Add named styles
930        if (themeName != null) {
931            for (Identifiable object : getNamedObjects(themeName, "style")) {
932                if (!(object instanceof Style)) {
933                    log.error("Expected Style object, got instead " + object);
934                    continue;
935                }
936                styles.add((Style) object);
937            }
938        }
939        return styles;
940    }
941
942    public List<Style> getSortedNamedStyles(String themeName) {
943        List<Style> namedStyles = getNamedStyles(themeName);
944
945        // sort styles to have a deterministic topological sort
946        List<String> names = new ArrayList<>(namedStyles.size());
947        Map<String, Style> allStyles = new HashMap<>();
948        for (Style style : namedStyles) {
949            String name = style.getName();
950            names.add(name);
951            allStyles.put(name, style);
952        }
953        Collections.sort(names);
954        namedStyles = new ArrayList<>(namedStyles.size());
955        for (String name : names) {
956            namedStyles.add(allStyles.get(name));
957        }
958
959        DAG graph = new DAG();
960        for (Style s : namedStyles) {
961            String styleName = s.getName();
962            graph.addVertex(styleName);
963            for (Format f : listFormatsDirectlyInheritingFrom(s)) {
964                if (!f.isNamed()) {
965                    continue;
966                }
967                try {
968                    graph.addEdge(styleName, f.getName());
969                } catch (CycleDetectedException e) {
970                    log.error("Cycle detected in style dependencies: ", e);
971                    return namedStyles;
972                }
973            }
974        }
975
976        List<Style> styles = new ArrayList<Style>();
977        for (Object name : TopologicalSorter.sort(graph)) {
978            styles.add((Style) getNamedObject(themeName, "style", (String) name));
979        }
980        return styles;
981    }
982
983    // Cache management
984    public Long getLastModified(String themeName) {
985        final Long date = lastModified.get(themeName);
986        if (date == null) {
987            return 0L;
988        }
989        return date;
990    }
991
992    public void setLastModified(String themeName, Long date) {
993        lastModified.put(themeName, date);
994    }
995
996    public Long getLastModified(final URL url) {
997        final String themeName = getThemeNameByUrl(url);
998        return getLastModified(themeName);
999    }
1000
1001    public void themeModified(String themeName) {
1002        setLastModified(themeName, new Date().getTime());
1003        Manager.getResourceManager().clearGlobalCache(themeName);
1004    }
1005
1006    public void stylesModified(String themeName) {
1007        resetCachedStyles(themeName);
1008    }
1009
1010    /**
1011     * Reset cached static resources, useful for hot reload
1012     *
1013     * @since 5.6
1014     */
1015    public void resetCachedResources() {
1016        cachedResources.clear();
1017    }
1018
1019    // Registration
1020    public void registerTheme(final ThemeElement theme) {
1021        String themeName = theme.getName();
1022
1023        // store to the theme
1024        themes.put(themeName, theme);
1025
1026        // store the pages
1027        for (Node node : theme.getChildren()) {
1028            PageElement page = (PageElement) node;
1029            String pagePath = String.format("%s/%s", themeName, page.getName());
1030            pages.put(pagePath, page);
1031        }
1032
1033        // hook to notify potential listeners that the theme was registered
1034        EventService eventService = Framework.getLocalService(EventService.class);
1035        eventService.sendEvent(new Event(THEME_TOPIC, THEME_REGISTERED_EVENT_ID, this, themeName));
1036
1037        themeModified(themeName);
1038        stylesModified(themeName);
1039    }
1040
1041    public void registerPage(final ThemeElement theme, final PageElement page) throws NodeException {
1042        theme.addChild(page);
1043        String themeName = theme.getName();
1044        String pageName = page.getName();
1045        pages.put(String.format("%s/%s", themeName, pageName), page);
1046        log.debug("Added page: " + pageName + " to theme: " + themeName);
1047    }
1048
1049    public void unregisterTheme(final ThemeElement theme) {
1050        String themeName = theme.getName();
1051        // remove pages
1052        for (PageElement page : getPagesOf(theme)) {
1053            unregisterPage(page);
1054        }
1055        // remove theme
1056        themes.remove(themeName);
1057        log.debug("Removed theme: " + themeName);
1058    }
1059
1060    public void unregisterPage(PageElement page) {
1061        ThemeElement theme = (ThemeElement) page.getParent();
1062        if (theme == null) {
1063            log.debug("Page has no parent: " + page.getUid());
1064            return;
1065        }
1066        String themeName = theme.getName();
1067        String pageName = page.getName();
1068        pages.remove(String.format("%s/%s", themeName, pageName));
1069        log.debug("Removed page: " + pageName + " from theme: " + themeName);
1070    }
1071
1072    public static void loadTheme(ThemeDescriptor themeDescriptor) {
1073        themeDescriptor.setLoadingFailed(true);
1074        String src = themeDescriptor.getSrc();
1075        if (src == null) {
1076            log.error("Could not load theme, source not set. ");
1077            return;
1078        }
1079        try {
1080            final boolean preload = false;
1081            ThemeParser.registerTheme(themeDescriptor, preload);
1082            themeDescriptor.setLoadingFailed(false);
1083        } catch (ThemeIOException e) {
1084            log.error("Could not register theme: " + src + " " + e.getMessage());
1085        }
1086    }
1087
1088    // Theme management
1089    public void loadTheme(String src, String xmlSource) throws ThemeIOException, ThemeException {
1090        ThemeDescriptor themeDescriptor = getThemeDescriptor(src);
1091        if (themeDescriptor == null) {
1092            throw new ThemeIOException("Theme not found: " + src);
1093        }
1094        final String oldThemeName = themeDescriptor.getName();
1095        themeDescriptor.setLoadingFailed(true);
1096        final boolean preload = false;
1097        ThemeParser.registerTheme(themeDescriptor, xmlSource, preload);
1098        String themeName = themeDescriptor.getName();
1099        themeDescriptor.setName(themeName);
1100
1101        themeModified(themeName);
1102        stylesModified(themeName);
1103        updateThemeDescriptors();
1104        // remove or restore customized themes
1105        if (!themeName.equals(oldThemeName)) {
1106            themes.remove(oldThemeName);
1107            for (ThemeDescriptor themeDef : getThemeDescriptors()) {
1108                if (oldThemeName.equals(themeDef.getName()) && !themeDef.isCustomized()) {
1109                    loadTheme(themeDef.getSrc());
1110                }
1111            }
1112        }
1113    }
1114
1115    public void loadTheme(String src) throws ThemeIOException, ThemeException {
1116        loadTheme(src, null);
1117    }
1118
1119    public void deleteTheme(String src) throws ThemeIOException, ThemeException {
1120        ThemeDescriptor themeDescriptor = getThemeDescriptor(src);
1121        if (themeDescriptor.isXmlConfigured()) {
1122            throw new ThemeIOException("Themes registered as contributions cannot be deleted: " + src);
1123        }
1124        final ThemeManager themeManager = Manager.getThemeManager();
1125        final String themeName = themeDescriptor.getName();
1126        ThemeElement theme = themeManager.getThemeByName(themeName);
1127        if (theme == null) {
1128            throw new ThemeIOException("Theme not found: " + themeName);
1129        }
1130
1131        URL url = null;
1132        try {
1133            url = new URL(src);
1134        } catch (MalformedURLException e) {
1135            throw new ThemeIOException(e);
1136        }
1137
1138        if (!url.getProtocol().equals("file")) {
1139            throw new ThemeIOException("Theme source is not that of a file: " + src);
1140        }
1141
1142        final File file = new File(url.getFile());
1143        if (!file.exists()) {
1144            throw new ThemeIOException("File not found: " + src);
1145        }
1146
1147        final String themeFileName = String.format("theme-%s.bak", themeName);
1148        final File backupFile = new File(getThemeDir(), themeFileName);
1149        if (backupFile.exists()) {
1150            if (!backupFile.delete()) {
1151                throw new ThemeIOException("Error while deleting backup file: " + backupFile.getPath());
1152            }
1153        }
1154        if (!file.renameTo(backupFile)) {
1155            throw new ThemeIOException("Error while creating backup file: " + backupFile.getPath());
1156        }
1157
1158        try {
1159            themeManager.destroyElement(theme);
1160        } catch (NodeException e) {
1161            throw new ThemeIOException("Failed to delete theme: " + themeName, e);
1162        } catch (ThemeException e) {
1163            throw new ThemeIOException("Failed to delete theme: " + themeName, e);
1164        }
1165
1166        themes.remove(themeName);
1167        deleteThemeDescriptor(src);
1168
1169        updateThemeDescriptors();
1170
1171        for (ThemeDescriptor themeDef : getThemeDescriptors()) {
1172            if (themeName.equals(themeDef.getName()) && !themeDef.isCustomized()) {
1173                loadTheme(themeDef.getSrc());
1174            }
1175        }
1176    }
1177
1178    public void deletePage(String path) throws ThemeIOException, ThemeException {
1179        PageElement page = getPageByPath(path);
1180        if (page == null) {
1181            throw new ThemeIOException("Failed to delete unkown page: " + path);
1182        }
1183        try {
1184            destroyElement(page);
1185        } catch (NodeException e) {
1186            throw new ThemeIOException("Failed to delete page: " + path, e);
1187        }
1188    }
1189
1190    public static void saveTheme(final String src) throws ThemeIOException, ThemeException {
1191        saveTheme(src, DEFAULT_THEME_INDENT);
1192    }
1193
1194    public static void saveTheme(final String src, final int indent) throws ThemeIOException, ThemeException {
1195        ThemeDescriptor themeDescriptor = getThemeDescriptor(src);
1196
1197        if (themeDescriptor == null) {
1198            throw new ThemeIOException("Theme not found: " + src);
1199        }
1200
1201        if (!themeDescriptor.isWritable()) {
1202            throw new ThemeIOException("Protocol does not support output: " + src);
1203        }
1204
1205        ThemeSerializer serializer = new ThemeSerializer();
1206        final String xml = serializer.serializeToXml(src, indent);
1207
1208        // Write the file
1209        URL url = null;
1210        try {
1211            url = new URL(src);
1212        } catch (MalformedURLException e) {
1213            throw new ThemeIOException("Could not save theme to " + src, e);
1214        }
1215        try {
1216            Utils.writeFile(url, xml);
1217        } catch (IOException e) {
1218            throw new ThemeIOException("Could not save theme to " + src, e);
1219        }
1220        themeDescriptor.setLastSaved(new Date());
1221        log.debug("Saved theme: " + src);
1222    }
1223
1224    public static void repairTheme(ThemeElement theme) throws ThemeIOException {
1225        try {
1226            ThemeRepairer.repair(theme);
1227        } catch (ThemeException e) {
1228            throw new ThemeIOException("Could not repair theme: " + theme.getName(), e);
1229        }
1230        log.debug("Repaired theme: " + theme.getName());
1231    }
1232
1233    public static String renderElement(URL url) throws ThemeException {
1234        String result = null;
1235        InputStream is = null;
1236        try {
1237            is = url.openStream();
1238            Reader in = null;
1239            try {
1240                in = new BufferedReader(new InputStreamReader(is));
1241                StringBuilder rendered = new StringBuilder();
1242                int ch;
1243                while ((ch = in.read()) > -1) {
1244                    rendered.append((char) ch);
1245                }
1246                result = rendered.toString();
1247            } catch (IOException e) {
1248                throw new ThemeException(e);
1249            } finally {
1250                if (in != null) {
1251                    in.close();
1252                }
1253            }
1254        } catch (IOException e) {
1255            throw new ThemeException(e);
1256        } finally {
1257            if (is != null) {
1258                try {
1259                    is.close();
1260                } catch (IOException e) {
1261                    log.error(e, e);
1262                } finally {
1263                    is = null;
1264                }
1265            }
1266        }
1267        return result;
1268    }
1269
1270    public void removeOrphanedFormats() throws ThemeException {
1271        UidManager uidManager = Manager.getUidManager();
1272        for (Format format : listFormats()) {
1273            // Skip named formats since they are not directly associated to an
1274            // element.
1275            if (format.isNamed()) {
1276                continue;
1277            }
1278            if (ElementFormatter.getElementsFor(format).isEmpty()) {
1279                deleteFormat(format);
1280                uidManager.unregister(format);
1281            }
1282        }
1283    }
1284
1285    private static void removeRelationsOf(Element element) {
1286        UidManager uidManager = Manager.getUidManager();
1287        PerspectiveManager perspectiveManager = Manager.getPerspectiveManager();
1288        for (Format format : ElementFormatter.getFormatsFor(element)) {
1289            ElementFormatter.removeFormat(element, format);
1290        }
1291        perspectiveManager.setAlwaysVisible(element);
1292        uidManager.unregister(element);
1293    }
1294
1295    private static void destroyDescendants(Element element) throws NodeException {
1296        for (Node node : element.getDescendants()) {
1297            removeRelationsOf((Element) node);
1298        }
1299        element.removeDescendants();
1300    }
1301
1302    // Format inheritance
1303    public void makeFormatInherit(Format format, Format ancestor) {
1304        if (format.equals(ancestor)) {
1305            FormatType formatType = format.getFormatType();
1306            String formatName = formatType != null ? formatType.getTypeName() : "unknown";
1307            log.error(String.format("A format ('%s' with type '%s') cannot inherit from itself, aborting",
1308                    format.getName(), formatName));
1309            return;
1310        }
1311        if (listAncestorFormatsOf(ancestor).contains(format)) {
1312            log.error("Cycle detected.in format inheritance, aborting.");
1313            return;
1314        }
1315        // remove old inheritance relations
1316        removeInheritanceTowards(format);
1317        // set new ancestor
1318        DyadicRelation relation = new DyadicRelation(PREDICATE_FORMAT_INHERIT, format, ancestor);
1319        Manager.getRelationStorage().add(relation);
1320    }
1321
1322    public static void removeInheritanceTowards(Format descendant) {
1323        Collection<Relation> relations = Manager.getRelationStorage().search(PREDICATE_FORMAT_INHERIT, descendant, null);
1324        Iterator<Relation> it = relations.iterator();
1325        while (it.hasNext()) {
1326            Relation relation = it.next();
1327            Manager.getRelationStorage().remove(relation);
1328        }
1329    }
1330
1331    public static void removeInheritanceFrom(Format ancestor) {
1332        Collection<Relation> relations = Manager.getRelationStorage().search(PREDICATE_FORMAT_INHERIT, null, ancestor);
1333        Iterator<Relation> it = relations.iterator();
1334        while (it.hasNext()) {
1335            Relation relation = it.next();
1336            Manager.getRelationStorage().remove(relation);
1337        }
1338    }
1339
1340    public static Format getAncestorFormatOf(Format format) {
1341        Collection<Relation> relations = Manager.getRelationStorage().search(PREDICATE_FORMAT_INHERIT, format, null);
1342        Iterator<Relation> it = relations.iterator();
1343        if (it.hasNext()) {
1344            return (Format) it.next().getRelate(2);
1345        }
1346        return null;
1347    }
1348
1349    public static List<Format> listAncestorFormatsOf(Format format) {
1350        List<Format> ancestors = new ArrayList<Format>();
1351        Format current = format;
1352        while (current != null) {
1353            current = getAncestorFormatOf(current);
1354            if (current == null) {
1355                break;
1356            }
1357            // cycle detected
1358            if (ancestors.contains(current)) {
1359                break;
1360            }
1361            ancestors.add(current);
1362        }
1363        return ancestors;
1364    }
1365
1366    public static List<Format> listFormatsDirectlyInheritingFrom(Format format) {
1367        List<Format> formats = new ArrayList<Format>();
1368        Collection<Relation> relations = Manager.getRelationStorage().search(PREDICATE_FORMAT_INHERIT, null, format);
1369        Iterator<Relation> it = relations.iterator();
1370        while (it.hasNext()) {
1371            formats.add((Format) it.next().getRelate(1));
1372        }
1373        return formats;
1374    }
1375
1376    public void deleteFormat(Format format) throws ThemeException {
1377        ThemeManager.removeInheritanceTowards(format);
1378        ThemeManager.removeInheritanceFrom(format);
1379        unregisterFormat(format);
1380    }
1381
1382    public static List<String> getUnusedStyleViews(Style style) {
1383        List<String> views = new ArrayList<String>();
1384        if (style.isNamed()) {
1385            return views;
1386        }
1387        for (Element element : ElementFormatter.getElementsFor(style)) {
1388            Widget widget = (Widget) ElementFormatter.getFormatFor(element, "widget");
1389            String viewName = widget.getName();
1390            for (String name : style.getSelectorViewNames()) {
1391                if (!name.equals(viewName)) {
1392                    views.add(name);
1393                }
1394            }
1395        }
1396        return views;
1397    }
1398
1399    // Cached styles
1400    public String getCachedStyles(String themeName, String basePath, String collectionName) {
1401        String key = String.format("%s|%s|%s", themeName, basePath != null ? basePath : "",
1402                collectionName != null ? collectionName : "");
1403        return cachedStyles.get(key);
1404    }
1405
1406    public synchronized void setCachedStyles(String themeName, String basePath, String collectionName, String css) {
1407        String key = String.format("%s|%s|%s", themeName, basePath != null ? basePath : "",
1408                collectionName != null ? collectionName : "");
1409        cachedStyles.put(key, css);
1410    }
1411
1412    private synchronized void resetCachedStyles(String themeName) {
1413        for (String key : cachedStyles.keySet()) {
1414            if (key.startsWith(themeName)) {
1415                cachedStyles.put(key, null);
1416            }
1417        }
1418    }
1419
1420    // Resources
1421    public String getResource(String name) {
1422        return cachedResources.get(name);
1423    }
1424
1425    public synchronized void setResource(String name, String content) {
1426        cachedResources.put(name, content);
1427    }
1428
1429    public synchronized void updateResourceOrdering() {
1430        DAG graph = new DAG();
1431        for (Type type : Manager.getTypeRegistry().getTypes(TypeFamily.RESOURCE)) {
1432            ResourceType resourceType = (ResourceType) type;
1433            String resourceName = resourceType.getName();
1434            graph.addVertex(resourceName);
1435            for (String dependency : resourceType.getDependencies()) {
1436                try {
1437                    graph.addEdge(resourceName, dependency);
1438                } catch (CycleDetectedException e) {
1439                    log.error("Cycle detected in resource dependencies: ", e);
1440                    return;
1441                }
1442            }
1443        }
1444        resourceOrdering.clear();
1445        for (Object r : TopologicalSorter.sort(graph)) {
1446            resourceOrdering.add((String) r);
1447        }
1448    }
1449
1450    public List<String> getResourceOrdering() {
1451        return resourceOrdering;
1452    }
1453
1454    /**
1455     * Returns all the ordered resource names and their dependencies, given a list of resources names.
1456     *
1457     * @since 5.5
1458     * @param resourceNames
1459     */
1460    // TODO: optimize?
1461    public List<String> getOrderedResourcesAndDeps(List<String> resourceNames) {
1462        List<String> res = new ArrayList<String>();
1463        if (resourceNames == null) {
1464            return res;
1465        }
1466        TypeRegistry typeRegistry = Manager.getTypeRegistry();
1467        for (String resourceName : resourceNames) {
1468            ResourceType resource = (ResourceType) typeRegistry.lookup(TypeFamily.RESOURCE, resourceName);
1469            if (resource == null) {
1470                log.error(String.format("Resource not registered %s.", resourceName));
1471                continue;
1472            }
1473            String[] deps = resource.getDependencies();
1474            if (deps != null) {
1475                for (String dep : deps) {
1476                    res.add(dep);
1477                }
1478            }
1479            res.add(resourceName);
1480        }
1481        List<String> orderedRes = new ArrayList<String>();
1482        List<String> ordered = getResourceOrdering();
1483        if (ordered != null) {
1484            for (String resource : ordered) {
1485                if (res.contains(resource)) {
1486                    orderedRes.add(resource);
1487                }
1488            }
1489        }
1490        return orderedRes;
1491    }
1492
1493    public void unregisterResourceOrdering(ResourceType resourceType) {
1494        String resourceName = resourceType.getName();
1495        if (resourceOrdering.contains(resourceName)) {
1496            resourceOrdering.remove(resourceName);
1497        }
1498    }
1499
1500    public byte[] getImageResource(String path) throws ThemeException {
1501        String key = String.format("image/%s", path);
1502        byte[] data = cachedBinaries.get(key);
1503        if (data == null) {
1504            String[] parts = path.split("/");
1505            if (parts.length != 3) {
1506                throw new ThemeException("Incorrect image path: " + path);
1507            }
1508            String resourceBankName = parts[0];
1509            String collectionName = parts[1];
1510            String resourceName = parts[2];
1511            data = ResourceManager.getBinaryBankResource(resourceBankName, collectionName, "image", resourceName);
1512            cachedBinaries.put(key, data);
1513        }
1514        return data;
1515    }
1516
1517    public static List<ViewType> getViewTypesForFragmentType(final FragmentType fragmentType) {
1518        final List<ViewType> viewTypes = new ArrayList<ViewType>();
1519
1520        for (Type v : Manager.getTypeRegistry().getTypes(TypeFamily.VIEW)) {
1521            final ViewType viewType = (ViewType) v;
1522            final String viewName = viewType.getViewName();
1523
1524            if ("*".equals(viewName)) {
1525                continue;
1526            }
1527
1528            // select fragment views
1529            final ElementType elementType = viewType.getElementType();
1530            if (elementType != null && !elementType.getTypeName().equals("fragment")) {
1531                continue;
1532            }
1533
1534            // select widget view types
1535            if (!viewType.getFormatType().getTypeName().equals("widget")) {
1536                continue;
1537            }
1538
1539            // match model types
1540            final ModelType modelType = viewType.getModelType();
1541            if (fragmentType.getModelType() != modelType) {
1542                continue;
1543            }
1544            viewTypes.add(viewType);
1545        }
1546        return viewTypes;
1547    }
1548
1549    // Resource banks
1550    public static ResourceBank getResourceBank(String name) throws ThemeException {
1551        if (name == null) {
1552            throw new ThemeException("Resource bank name not set");
1553        }
1554        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
1555        ResourceBank resourceBank = (ResourceBank) typeRegistry.lookup(TypeFamily.RESOURCE_BANK, name);
1556        if (resourceBank != null) {
1557            return resourceBank;
1558        } else {
1559            throw new ThemeException("Resource bank not found: " + name);
1560        }
1561    }
1562
1563    public static List<ResourceBank> getResourceBanks() {
1564        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
1565        List<ResourceBank> resourceBanks = new ArrayList<ResourceBank>();
1566        for (Type type : typeRegistry.getTypes(TypeFamily.RESOURCE_BANK)) {
1567            resourceBanks.add((ResourceBank) type);
1568        }
1569        return resourceBanks;
1570    }
1571
1572    // Theme descriptors
1573    public static List<ThemeDescriptor> getThemeDescriptors() {
1574        final List<ThemeDescriptor> themeDescriptors = new ArrayList<ThemeDescriptor>();
1575        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
1576        for (Type type : typeRegistry.getTypes(TypeFamily.THEME)) {
1577            if (type != null) {
1578                ThemeDescriptor themeDescriptor = (ThemeDescriptor) type;
1579                themeDescriptors.add(themeDescriptor);
1580            }
1581        }
1582        return themeDescriptors;
1583    }
1584
1585    public static ThemeDescriptor getThemeDescriptor(String src) throws ThemeException {
1586        ThemeDescriptor themeDef = (ThemeDescriptor) Manager.getTypeRegistry().lookup(TypeFamily.THEME, src);
1587        if (themeDef == null) {
1588            throw new ThemeException("Unknown theme: " + src);
1589        }
1590        return themeDef;
1591    }
1592
1593    public static void deleteThemeDescriptor(String src) throws ThemeException {
1594        ThemeDescriptor themeDef = getThemeDescriptor(src);
1595        Manager.getTypeRegistry().unregister(themeDef);
1596    }
1597
1598    // Template engines
1599    public static List<String> getTemplateEngineNames() {
1600        List<String> types = new ArrayList<String>();
1601        for (Type type : Manager.getTypeRegistry().getTypes(TypeFamily.TEMPLATE_ENGINE)) {
1602            types.add(type.getTypeName());
1603        }
1604        return types;
1605    }
1606
1607    public static String getTemplateEngineName(String applicationPath) {
1608        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
1609        if (applicationPath == null) {
1610            return ThemeManager.getDefaultTemplateEngineName();
1611        }
1612        final ApplicationType application = (ApplicationType) typeRegistry.lookup(TypeFamily.APPLICATION,
1613                applicationPath);
1614
1615        if (application != null) {
1616            return application.getTemplateEngine();
1617        }
1618        return getDefaultTemplateEngineName();
1619    }
1620
1621    public static String getDefaultTemplateEngineName() {
1622        // TODO use XML configuration
1623        return "jsf-facelets";
1624    }
1625
1626    public static Element getElementById(final Integer id) {
1627        Object object = Manager.getUidManager().getObjectByUid(id);
1628        if (!(object instanceof Element)) {
1629            return null;
1630        }
1631        return (Element) object;
1632    }
1633
1634    public static Element getElementById(final String id) {
1635        return getElementById(Integer.valueOf(id));
1636    }
1637
1638    public static Format getFormatById(final Integer id) {
1639        Object object = Manager.getUidManager().getObjectByUid(id);
1640        if (!(object instanceof Format)) {
1641            return null;
1642        }
1643        return (Format) object;
1644    }
1645
1646    public static Format getFormatById(final String id) {
1647        return getFormatById(Integer.valueOf(id));
1648    }
1649
1650    public static ThemeElement getThemeOfFormat(Format format) {
1651        Collection<Element> elements = ElementFormatter.getElementsFor(format);
1652        if (elements.isEmpty()) {
1653            return null;
1654        }
1655        // Get the first element assuming all elements belong to the same
1656        // theme.
1657        Element element = elements.iterator().next();
1658        return getThemeOf(element);
1659    }
1660
1661    public Layout createLayout() {
1662        Layout layout = null;
1663        try {
1664            layout = (Layout) FormatFactory.create("layout");
1665            registerFormat(layout);
1666        } catch (ThemeException e) {
1667            log.error("Layout creation failed", e);
1668        }
1669        return layout;
1670    }
1671
1672    public Widget createWidget() {
1673        Widget widget = null;
1674        try {
1675            widget = (Widget) FormatFactory.create("widget");
1676            registerFormat(widget);
1677        } catch (ThemeException e) {
1678            log.error("Widget creation failed", e);
1679        }
1680        return widget;
1681    }
1682
1683    public Style createStyle() {
1684        Style style = null;
1685        try {
1686            style = (Style) FormatFactory.create("style");
1687            registerFormat(style);
1688        } catch (ThemeException e) {
1689            log.error("Style creation failed", e);
1690        }
1691        return style;
1692    }
1693
1694    public void registerModelByClassname(ModelType modelType) {
1695        modelsByClassname.put(modelType.getClassName(), modelType);
1696    }
1697
1698    public void unregisterModelByClassname(ModelType modelType) {
1699        modelsByClassname.remove(modelType.getClassName());
1700    }
1701
1702    public ModelType getModelByClassname(String className) {
1703        return modelsByClassname.get(className);
1704    }
1705
1706    // Theme sets
1707    public List<ThemeSet> getThemeSets() {
1708        List<ThemeSet> themeSets = new ArrayList<ThemeSet>();
1709        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
1710        for (Type type : typeRegistry.getTypes(TypeFamily.THEMESET)) {
1711            themeSets.add((ThemeSet) type);
1712        }
1713        return themeSets;
1714    }
1715
1716    public ThemeSet getThemeSetByName(final String name) {
1717        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
1718        return (ThemeSet) typeRegistry.lookup(TypeFamily.THEMESET, name);
1719    }
1720
1721    public static String getCollectionCssMarker() {
1722        return COLLECTION_CSS_MARKER;
1723    }
1724
1725}