org.apache.myfaces.trinidadinternal.style.util.StableNameUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trinidad-impl Show documentation
Show all versions of trinidad-impl Show documentation
Private implementation of the Apache MyFaces Trinidad project
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.trinidadinternal.style.util;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.TreeSet;
import org.apache.myfaces.trinidad.context.Version;
import org.apache.myfaces.trinidad.util.Range;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidadinternal.agent.TrinidadAgent.Application;
import org.apache.myfaces.trinidadinternal.skin.AgentAtRuleMatcher;
import org.apache.myfaces.trinidadinternal.style.StyleContext;
import org.apache.myfaces.trinidadinternal.style.util.StyleSheetVisitUtils.StyleSheetVisitor;
import org.apache.myfaces.trinidadinternal.style.xml.XMLConstants;
import org.apache.myfaces.trinidadinternal.style.xml.parse.StyleSheetDocument;
import org.apache.myfaces.trinidadinternal.style.xml.parse.StyleSheetNode;
import org.apache.myfaces.trinidadinternal.util.nls.LocaleUtils;
/**
* This class serves one purpose: it provides a replacement for NameUtils.getContextName()
* that produces "stable" file names. This ensures that generated style sheets will be
* named in a consistent manner that is not dependent on the order in which requests arrives.
*
* See the "Structure of Stable Style Sheet Names" section in the Skinning chapter of the
* Trinidad Developer's Guide for the specification of stable style sheet names.
*/
public final class StableNameUtils
{
/**
* Similar to NameUtils.getContextName(), but produces stable names.
*
* @param context the current style context
* @param document the skin document
* @return a name suitable for inclusion in a generated style sheet file name.
*/
public static String getContextName(
StyleContext context,
StyleSheetDocument document
)
{
return new ContextNameProducer(context, document).getName();
}
// Helper class that produces context-specific file names.
private static final class ContextNameProducer
{
// The ContextNameProducer implementation is designed for modularity/code
// cleanliness more than performance. Each name that is produced generates
// a fairly large number of temporary objects (most iterators). This seems
// acceptable as we only generate each name once per generated file, when the
// file is initially written. If performance/temporary object creation
// ever become an issue, we can optimize/refactor.
public ContextNameProducer(
StyleContext context,
StyleSheetDocument document
)
{
_context = context;
_documentId = document.getDocumentId(context);
_styleSheets = document.getStyleSheetsAsCollection(context);
}
/**
* Returns the context-specific file name.
*/
public String getName()
{
// We're going to collect all required info (eg. platform, agent,
// locale, etc...) in a single StyleSheetVisitUtils.visit(), during
// which we delegate to a number of "naming" StyleSheetVisitors
// to collect the info that we need to produce a name.
Collection visitors = _getVisitors();
_visitStyleSheets(visitors);
return _toName(visitors);
}
private Collection _getVisitors()
{
return Arrays.asList(
new PlatformNameExtractor(),
new ApplicationNameExtractor(_context.getAgent().getVersion()),
new LocaleNameExtractor(),
new DirectionNameExtractor(),
new AccessibilityNameExtractor());
}
private void _visitStyleSheets(Collection extends StyleSheetVisitor> visitors)
{
StyleSheetVisitor compoundVisitor = StyleSheetVisitUtils.compoundStyleSheetVisitor(visitors);
StyleSheetVisitUtils.visitStyleSheets(_styleSheets, compoundVisitor);
}
private String _toName(Collection visitors)
{
// Picking a slightly large initial size in the hopes that this will
// avoid reallocations.
StringBuilder builder = new StringBuilder(100);
_appendSkinIdentifierSection(builder);
_appendVariantsSection(builder, visitors);
_appendContextualSection(builder);
_appendSuffix(builder);
return builder.toString();
}
private void _appendSkinIdentifierSection(StringBuilder builder)
{
// The document id is actually just the content/version hash. The skin
// id is later prepended in SkinStyleProvider.getTargetStyleSheetName().
builder.append(_documentId);
// Double-separator to make it easier to write regular
// expressions against the interesting portion of the name
// (eg. for uri rewriting purposes).
builder.append(_SEPARATOR);
builder.append(_SEPARATOR);
}
private void _appendVariantsSection(
StringBuilder builder,
Collection visitors
)
{
for (NamingStyleSheetVisitor visitor : visitors)
{
visitor.appendName(builder);
builder.append(_SEPARATOR);
}
// Another double-dash separator to aid regular expression
// targeting. This may come in handy if we ever end up
// introducing new variants that we want to add to the
// visit-based names section.
builder.append(_SEPARATOR);
}
private void _appendContextualSection(StringBuilder builder)
{
builder.append(_context.isPortletMode() ? "p" : "s");
builder.append(_SEPARATOR);
builder.append(_context.isRequestSecure() ? "s" : "n");
builder.append(_SEPARATOR);
builder.append(_context.isDisableStyleCompression() ? "u" : "c");
}
private void _appendSuffix(StringBuilder builder)
{
builder.append(".css");
}
private final StyleContext _context;
private final String _documentId;
private final Collection _styleSheets;
}
// Extension of StyleSheetVisitor that contributes to name generation
private interface NamingStyleSheetVisitor extends StyleSheetVisitor
{
/**
* Appends the the file name that is being built up in the specified
* StringBuilder.
*/
public void appendName(StringBuilder builder);
}
// Abstract base class for NamingStyleSheetVisitors that operate on the
// collections coughed up by StyleSheetNode. Handles three possible
// scenarios:
//
// 1. There is no explicit match - eg. the skin does not define any @locale
// rules, so StyleSheetNode.getLocales() is always empty. In this case,
// we always append the default match path token.
// 2. There is a single match - eg. the skin defines "@locale ja" and this
// StyleSheetNode is visited. In this case, we append the string representation
// of the matched value.
// 3. There is a multiple match (and no single match) - eg. the skin defines
// "@locale ja, cz, ko" and this StyleSheetNode is visited. In this case,
// we append a concatenation of the string representation of each value,
// separated by the underscore character.
private abstract static class CollectionNameExtractor implements NamingStyleSheetVisitor
{
/**
* Returns the collection of values that contribute to the name.
*/
abstract protected Collection getCollectionValues(StyleSheetNode styleSheet);
@Override
public void visit(StyleSheetNode styleSheet)
{
Collection newValues = getCollectionValues(styleSheet);
if (!newValues.isEmpty())
{
if (_values == null)
{
// Need to make a copy since we'll be modifying this collection.
_values = new HashSet(newValues);
}
else
{
mergeValues(styleSheet, _values, newValues);
// We should never reach a state where the collected values
// is empty. This would indicate that our style sheet node
// matching logic is wrong - eg. we matched a style sheet
// node with "@locale ja" and a second style sheet node with
// "@locale ko". Make some noise if we hit this.
if (_values.isEmpty())
{
_fail();
}
}
}
}
/**
* Merges previously collected values with a new collection of values,
* storing the result in oldValues.
*
* By default, the intersection of the two collections is retained.
*/
protected void mergeValues(
StyleSheetNode styleSheet,
Collection oldValues,
Collection newValues
)
{
oldValues.retainAll(newValues);
}
@Override
public void appendName(StringBuilder builder)
{
if (_values == null)
{
builder.append(_DEFAULT_PATH_TOKEN);
}
else
{
assert(_values != null);
assert(!_values.isEmpty());
Collection names = _toNames(_values);
_appendNames(builder, names);
}
}
private Collection _toNames(Collection values)
{
Collection names = new TreeSet();
for (E value : values)
{
String name = toName(value);
assert(name != null);
names.add(name);
}
return names;
}
/**
* Converts the specified value to a name to include in the
* file name.
*/
protected String toName(E value)
{
return value.toString();
}
private void _appendNames(StringBuilder builder, Collection names)
{
assert(names != null);
assert(!names.isEmpty());
Iterator iter = names.iterator();
while (iter.hasNext())
{
builder.append(iter.next());
if (iter.hasNext())
{
builder.append("_");
}
}
}
private Collection _values = null;
}
// NamingStyleSheetVisitor that extracts the platform name.
private static final class PlatformNameExtractor extends CollectionNameExtractor
{
@Override
protected Collection getCollectionValues(StyleSheetNode styleSheet)
{
return styleSheet.getPlatforms();
}
@Override
protected String toName(Integer platform)
{
return NameUtils.getPlatformName(platform);
}
}
// NamingStyleSheetVisitor that extracts the agent application name
private static class ApplicationNameExtractor extends CollectionNameExtractor
{
public ApplicationNameExtractor(Version agentVersion)
{
assert(agentVersion != null);
_agentVersion = agentVersion;
_matchedVersions = Version.ALL_VERSIONS;
}
@Override
protected String toName(Application agentApplication)
{
return agentApplication.getAgentName();
}
@Override
protected Collection getCollectionValues(StyleSheetNode styleSheet)
{
AgentAtRuleMatcher agentMatcher = styleSheet.getAgentMatcher();
if (agentMatcher == null)
{
return Collections.emptySet();
}
return agentMatcher.getAllApplications();
}
@Override
protected void mergeValues(
StyleSheetNode styleSheet,
Collection oldAgentApplications,
Collection newAgentApplications
)
{
super.mergeValues(styleSheet, oldAgentApplications, newAgentApplications);
// In addition to merging application values, we also need to keep track of
// versions that we have seen, since this may be included in the generated
// file name. Note that we only care about versions for cases where we've
// got an exact agent application match (ie. in cases where we actually know
// which application we are targeted.)
Application newAgentApplication = _getSingleAgentApplication(newAgentApplications);
if (newAgentApplication != null)
{
_mergeVersions(styleSheet.getAgentMatcher(), newAgentApplication);
}
}
// If the collection contains a single entry, returns the single value.
// Otherwise, returns null;
private Application _getSingleAgentApplication(Collection agentApplications)
{
if (agentApplications.size() == 1)
{
Iterator iter = agentApplications.iterator();
if (iter.hasNext())
{
return iter.next();
}
}
return null;
}
private void _mergeVersions(
AgentAtRuleMatcher agentMatcher,
Application agentApplication
)
{
assert(agentMatcher != null);
assert(agentApplication != null);
Range matchedVersions =
agentMatcher.getMatchedVersionsForApplication(agentApplication, _agentVersion);
_matchedVersions = _matchedVersions.intersect(matchedVersions);
// We should never see an empty range at this point since the two
// ranges that we intersected both contains _agentVersion.
if (_matchedVersions.isEmpty())
{
_fail();
}
}
@Override
public void appendName(StringBuilder builder)
{
// This super call will handle appending the agent name segment.
// However, we also need to ensure that the agent version is added.
super.appendName(builder);
// Tack on a value for the version field. (All stable names need to
// have the same # of fields in order to support allow uris/names to
// be easily targeted by uri rewriting regular expressions.)
builder.append(_SEPARATOR);
_appendVersionName(builder);
}
private void _appendVersionName(StringBuilder builder)
{
Version startVersion = _matchedVersions.getStart();
Version endVersion = _matchedVersions.getEnd();
boolean startMin = startVersion.equals(Version.MIN_VERSION);
boolean endMax = endVersion.equals(Version.MAX_VERSION);
if (startMin && endMax)
{
builder.append(_DEFAULT_PATH_TOKEN);
}
else if (startMin)
{
builder.append(endVersion);
builder.append("m");
}
else if (endMax)
{
builder.append(startVersion);
builder.append("p");
}
else
{
builder.append(startVersion);
}
}
private final Version _agentVersion;
private Range _matchedVersions;
}
// NamingStyleSheetVisitor that extracts the locale name.
private static final class LocaleNameExtractor extends CollectionNameExtractor
{
@Override
protected Collection getCollectionValues(StyleSheetNode styleSheet)
{
return styleSheet.getLocales();
}
@Override
protected void mergeValues(
StyleSheetNode styleSheet,
Collection oldLocales,
Collection newLocales
)
{
assert(oldLocales != null);
assert(newLocales != null);
// We can't simply use Collection.retainAll() because we need to
// compensate for partial locales - ie. if "ja" is present in
// oldLocales and we encounter "ja_JP", Collection.retainAll()
// would result in the empty set, where as we want to replace "ja"
// with "ja_JP".
for (Locale newLocale : newLocales)
{
_retainLocale(oldLocales, newLocale);
}
}
// We only want to retain the new locale if:
//
// a) it exist in the old locales, or...
// b) one of its "super" locales exists in the old locales.
//
// In the case of b), we want to remove the super-locale and
// repalce it with the new, more-specific locale.
private void _retainLocale(Collection oldLocales, Locale newLocale)
{
// We could optimize by explicitly checking whether the new locale actually
// specifies a country/variant, but prefer to keep the code simple.
Locale langOnlyLocale = _toLanguageOnlyLocale(newLocale);
Locale langAndCountryLocale = _toLanguageAndCountryLocale(newLocale);
if (oldLocales.contains(langOnlyLocale) || oldLocales.contains(langAndCountryLocale))
{
oldLocales.remove(langOnlyLocale);
oldLocales.remove(langAndCountryLocale);
oldLocales.add(newLocale);
}
}
private Locale _toLanguageOnlyLocale(Locale locale)
{
return new Locale(locale.getLanguage());
}
private Locale _toLanguageAndCountryLocale(Locale locale)
{
return new Locale(locale.getLanguage(), locale.getCountry());
}
}
// NamingStyleSheetVisitor that extracts the direction name
private static class DirectionNameExtractor implements NamingStyleSheetVisitor
{
@Override
public void visit(StyleSheetNode styleSheet)
{
int direction = styleSheet.getReadingDirection();
if (direction != LocaleUtils.DIRECTION_DEFAULT)
{
assert((_direction == LocaleUtils.DIRECTION_DEFAULT) || (_direction == direction));
_direction = direction;
}
}
@Override
public void appendName(StringBuilder builder)
{
String name = (_direction == LocaleUtils.DIRECTION_DEFAULT) ?
_DEFAULT_PATH_TOKEN :
NameUtils.getDirectionName(_direction);
builder.append(name);
}
private int _direction = LocaleUtils.DIRECTION_DEFAULT;
}
// NamingStyleSheetVisitor that extracts the accessibility name
private static final class AccessibilityNameExtractor extends CollectionNameExtractor
{
@Override
protected Collection getCollectionValues(StyleSheetNode styleSheet)
{
return styleSheet.getAccessibilityProperties();
}
protected void mergeValues(
StyleSheetNode styleSheet,
Collection oldAccessibilityProperties,
Collection newAccessibilityProperties
)
{
oldAccessibilityProperties.addAll(newAccessibilityProperties);
}
@Override
protected String toName(String accessibilityProperty)
{
String name = null;
if (XMLConstants.ACC_HIGH_CONTRAST.equals(accessibilityProperty))
{
name = "hc";
}
if (XMLConstants.ACC_LARGE_FONTS.equals(accessibilityProperty))
{
name = "lf";
}
assert(name != null);
return name;
}
}
private static void _fail()
{
String message = _LOG.getMessage("SKIN_GENERATION_ERROR");
throw new IllegalStateException(message);
}
private StableNameUtils()
{
}
private static final char _SEPARATOR = '-';
private static final String _DEFAULT_PATH_TOKEN = "d";
private static final TrinidadLogger _LOG =
TrinidadLogger.createTrinidadLogger(StableNameUtils.class);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy