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