001    package org.apache.myfaces.tobago.renderkit.css;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one or more
005     * contributor license agreements.  See the NOTICE file distributed with
006     * this work for additional information regarding copyright ownership.
007     * The ASF licenses this file to You under the Apache License, Version 2.0
008     * (the "License"); you may not use this file except in compliance with
009     * the License.  You may obtain a copy of the License at
010     *
011     *      http://www.apache.org/licenses/LICENSE-2.0
012     *
013     * Unless required by applicable law or agreed to in writing, software
014     * distributed under the License is distributed on an "AS IS" BASIS,
015     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016     * See the License for the specific language governing permissions and
017     * limitations under the License.
018     */
019    
020    import org.apache.commons.collections.map.MultiKeyMap;
021    import org.apache.commons.lang.StringUtils;
022    import org.apache.myfaces.tobago.component.SupportsMarkup;
023    import org.apache.myfaces.tobago.context.Markup;
024    import org.apache.myfaces.tobago.context.Theme;
025    import org.apache.myfaces.tobago.internal.util.Deprecation;
026    import org.apache.myfaces.tobago.util.VariableResolverUtils;
027    import org.slf4j.Logger;
028    import org.slf4j.LoggerFactory;
029    
030    import javax.faces.component.UIComponent;
031    import javax.faces.context.FacesContext;
032    
033    /**
034     * Builds the CSS class attribute of tags.
035     * The names will be generated in a formal way, so generic name (and abbrevation) are possible.
036     * The class works like a factory, so caching will be possible.
037     * <p/>
038     * The default naming conventions allow these values:<br/>
039     *
040     * <ul>
041     * <li>tobago-&lt;rendererName></li>
042     * <li>tobago-&lt;rendererName>-markup-&lt;markupName></li>
043     * <li>tobago-&lt;rendererName>-&lt;subElement></li>
044     * <li>tobago-&lt;rendererName>-&lt;subElement>-markup-&lt;markupName></li>
045     * </ul>
046     *
047     * where
048     * <ul>
049     * <li>&lt;rendererName>, &lt;subElement> and &lt;markupName> must only contain ASCII-chars and -numbers</li>
050     * <li>&lt;rendererName> is the rendererType with a lower case char as first char</li>
051     * <li>&lt;subElement> is a sub element of the main tag in the output language (e.g. HTML)</li>
052     * <li>&lt;markupName> is the name of an existing markup</li>
053     * </ul>
054     * If the markup contains more than one name, there will be generated more than one output string.
055     * E.g.: UIIn with Markup [readonly, error] will get the class
056     * "tobago-in tobago-in-markup-readonly tobago-in-markup-error".
057     *
058     */
059    public final class Classes {
060    
061      private static final Logger LOG = LoggerFactory.getLogger(Classes.class);
062    
063      private static final MultiKeyMap CACHE = new MultiKeyMap();
064    
065      private final String stringValue;
066    
067      public static Classes create(UIComponent component) {
068        return create(component, true, null, null, false);
069      }
070    
071      public static Classes create(UIComponent component, String sub) {
072        return create(component, true, sub, null, false);
073      }
074    
075      public static Classes create(UIComponent component, Markup explicit) {
076        return create(component, false, null, explicit, false);
077      }
078    
079      public static Classes create(UIComponent component, String sub, Markup explicit) {
080        return create(component, false, sub, explicit, false);
081      }
082    
083      // XXX optimize synchronized
084      private static synchronized Classes create(
085          UIComponent component, boolean markupFromComponent, String sub, Markup explicit, boolean ignoreCheck) {
086        final String rendererName = StringUtils.uncapitalize(component.getRendererType());
087        final Markup markup = markupFromComponent ? ((SupportsMarkup) component).getCurrentMarkup() : explicit;
088        Classes value = (Classes) CACHE.get(rendererName, markup, sub);
089        if (value == null) {
090          value = new Classes(rendererName, markup, sub, ignoreCheck);
091          CACHE.put(rendererName, markup, sub, value);
092          if (LOG.isDebugEnabled()) {
093            LOG.debug("Element added (size={}) to cache (renderName='{}', markup='{}', sub='{}')",
094                new Object[] {CACHE.size(), rendererName, markup, sub});
095          }
096        }
097        return value;
098      }
099    
100      private Classes(String rendererName, Markup markup, String sub, boolean ignoreMarkupCheck) {
101    
102        assert sub == null || StringUtils.isAlphanumeric(sub) : "Invalid sub element name: '" + sub + "'";
103    
104        // These values are statistically tested length of the html class attribute
105        StringBuilder builder = new StringBuilder(markup != null ? 80 : 32);
106        builder.append("tobago-");
107        builder.append(rendererName);
108        if (sub != null) {
109          builder.append('-');
110          builder.append(sub);
111        }
112        if (markup != null) {
113          Theme theme = VariableResolverUtils.resolveClientProperties(FacesContext.getCurrentInstance()).getTheme();
114          for (String markupString : markup) {
115            if (ignoreMarkupCheck || theme.getRenderersConfig().isMarkupSupported(rendererName, markupString)) {
116              builder.append(' ');
117              builder.append("tobago-");
118              builder.append(rendererName);
119              if (sub != null) {
120                builder.append('-');
121                builder.append(sub);
122              }
123              builder.append("-markup-");
124              builder.append(markupString);
125            } else if ("none".equals(markupString)) {
126              Deprecation.LOG.warn("Markup 'none' is deprecated, please use a NULL pointer instead.");
127            } else {
128              LOG.warn("Ignoring unknown markup='" + markupString + "' for rendererName='" + rendererName + "'");
129            }
130          }
131        }
132        this.stringValue = builder.toString();
133      }
134    
135      public String getStringValue() {
136        return stringValue;
137      }
138    
139      /** @deprecated This workaround will be removed later */
140      @Deprecated
141      public static String requiredWorkaround(UIComponent component) {
142        final String rendererName = StringUtils.uncapitalize(component.getRendererType());
143        return "tobago-" + rendererName + "-markup-required";
144      }
145    
146      /**
147       * @deprecated This workaround will be removed later
148       */
149      @Deprecated
150      public static Classes createWorkaround(String rendererName, String sub, Markup explicit) {
151        return new Classes(rendererName, explicit, sub, false);
152      }
153    
154      /**
155       * @deprecated This workaround will be removed later
156       */
157      @Deprecated
158      public static Classes createWorkaround(String rendererName, Markup explicit) {
159        return new Classes(rendererName, explicit, null, false);
160      }
161    
162    }