danke für deine Antwort. Aber das scheint es nicht zu sein. Im Log des Generierungsauftrags finde ich nicht den geringsten Heinweis auf einen Konflikt. Ich hänge jetzt einfach mal den Quelltext der URLFactory an. Vielleicht kann ja von euch jemand erkennen, wieso z.B. eine Datei index-2.html erzeugt wird.
package de.namics.fs.urlfactory;
import de.espirit.common.StringUtil;
import de.espirit.common.io.IoError;
import de.espirit.firstspirit.access.Language;
import de.espirit.firstspirit.access.project.Resolution;
import de.espirit.firstspirit.access.project.TemplateSet;
import de.espirit.firstspirit.access.store.ContentProducer;
import de.espirit.firstspirit.access.store.IDProvider;
import de.espirit.firstspirit.access.store.LanguageInfo;
import de.espirit.firstspirit.access.store.PageParams;
import de.espirit.firstspirit.access.store.mediastore.File;
import de.espirit.firstspirit.access.store.mediastore.Media;
import de.espirit.firstspirit.access.store.mediastore.MediaMetaData;
import de.espirit.firstspirit.access.store.mediastore.Picture;
import de.espirit.firstspirit.access.store.sitestore.Content2Params;
import de.espirit.firstspirit.access.store.sitestore.ContentMultiPageParams.ContentPageParams;
import de.espirit.firstspirit.access.store.sitestore.PageRef;
import de.espirit.firstspirit.access.store.sitestore.SiteStoreFolder;
import de.espirit.firstspirit.generate.PathLookup;
import de.espirit.firstspirit.generate.UrlFactory;
import de.espirit.or.schema.Entity;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Search engine-optimized path factory.
*/
public class NamicsUrlFactory implements UrlFactory {
private PathLookup _pathLookup;
private boolean _useWelcomeFilenames;
/**
* Initialize fields based on various settings and a {@link PathLookup} object.
* @param settings Settings provided in module.xml file in section {@code <configuration>..</configuration>}.
* The key is the tag name (converted to lower case), value is the text child node. E.g. {@code <key>value</key>}.
* @param pathLookup Path lookup for user defined paths.
*/
@Override
public void init(final Map<String, String> settings, final PathLookup pathLookup) {
_pathLookup = pathLookup;
final String useWelcomeFilenames = settings.get("usewelcomefilenames");
_useWelcomeFilenames = useWelcomeFilenames == null || "yes".equalsIgnoreCase(useWelcomeFilenames) || "true".equalsIgnoreCase(useWelcomeFilenames);
}
/**
* Build the URL for a content-producing store element.
* @param contentProducer A store element.
* @param templateSet The target template set.
* @param language The target language.
* @param pageParams Page parameters, used for content projection, etc.
* @return The URL for the {@code contentProducer}, based on target template, target language and optional page parameters.
*/
@Override
public String getUrl(final ContentProducer contentProducer, final TemplateSet templateSet, final Language language, final PageParams pageParams) {
final String name = getName(contentProducer, templateSet, language, pageParams);
final String extension = contentProducer.getExtension(templateSet);
int len = name.length();
if (!extension.isEmpty()) {
len += extension.length();
len ++; // for dot
}
final StringBuilder buffer = new StringBuilder(0);
final String path = _pathLookup.lookupPath(contentProducer, language, templateSet);
if (path != null) {
buffer.ensureCapacity(len + path.length() + 2);
buffer.append('/');
if (!path.isEmpty()) {
buffer.append(path);
buffer.append('/');
}
} else {
collectPath(contentProducer.getParent(), language, templateSet, len, buffer);
}
buffer.append(name);
if (!extension.isEmpty()) {
buffer.append('.');
buffer.append(extension);
}
return buffer.toString();
}
/**
* Build the URL for a Media Store element.
* @param node The target node, located in the Media Store.
* @param language Target language or {@code null} for language-independent media nodes.
* @param resolution Target resolution or {@code null} for media nodes of type {@link de.espirit.firstspirit.access.store.mediastore.Media#FILE}.
* @return The URL for the {@code node}, based on target language and optional resolution.
*/
@Override
public String getUrl(final Media node, @Nullable final Language language, @Nullable final Resolution resolution) {
final String name = getName(node, language);
String resolutionString = null;
int len = name.length();
if ((resolution != null) && (node.getType() == Media.PICTURE) && !resolution.isOriginal()) {
resolutionString = resolution.getUid();
len += resolutionString.length();
len++; // for underscoure
}
final String extension = getExtension(node, language, resolution);
if (extension != null) {
len += extension.length();
len++; // for dot
}
final StringBuilder buffer = new StringBuilder(0);
collectPath(node.getParent(), language, null, len, buffer);
buffer.append(name);
if (resolutionString != null) {
buffer.append('_');
buffer.append(resolutionString);
}
if (extension != null) {
buffer.append('.');
buffer.append(extension);
}
return buffer.toString();
}
/**
* Build a name for the provided node.
*
* This implementation first attempts to identify if the node is a page reference that is used for content projection and displays only a single
* dataset; if so, the sitemap variable is used to form a name.
*
* If the node is not used for content projection, but the following two criteria are met:
* - the node is the start node of a Site Store folder
* - the configuration parameter "usewelcomefilenames" evaluates to "yes" or "true" (see {@code init(...)}
* the node is named "index".
*
* If none of the above criteria match, the language-dependent display name of the node is retrieved and used as the node's name in the URL that
* is being built.
*
* In all cases, the node names returned are processed using the {@code cleanup(...)} method.
* @param contentProducer The node to identify a URL name for.
* @param templateSet The template set for which to generate a URL name.
* @param language The project language for which to generate a URL name.
* @param pageParams Page parameters that may indicate if content projection is used.
* @return The name of this {@code contentProducer}, to be used in forming a URL for this element.
*/
private String getName(final ContentProducer contentProducer, final TemplateSet templateSet, final Language language, final PageParams pageParams) {
if ((contentProducer instanceof PageRef) && (pageParams instanceof ContentPageParams) && (pageParams.getSize() == 1)) {
final Content2Params content2Params = ((PageRef) contentProducer).getContent2Params();
if (content2Params != null) {
String varName = content2Params.getSitemapVariableName();
if (varName != null) {
if (varName.endsWith("*")) {
varName = varName.substring(0, varName.length() - 1);
}
final ContentPageParams contentPageParams = (ContentPageParams) pageParams;
final List<Entity> list = contentPageParams.getData();
if (!list.isEmpty()) {
final Entity entity = list.get(0);
final String result = resolve(entity, varName, language, "");
if ( ! StringUtil.isEmpty(result)) {
return cleanup(result);
}
}
}
}
}
if (_useWelcomeFilenames && (pageParams.getIndex() == 0) && ! (pageParams instanceof ContentPageParams)) {
final SiteStoreFolder folder = (SiteStoreFolder) contentProducer.getParent();
//noinspection ObjectEquality
if ((folder != null) && (folder.getStartNode() == contentProducer)) {
// check if we are the first template set for a specific extension
final String extension = templateSet.getExtension();
for (final TemplateSet set : contentProducer.getProject().getTemplateSets()) {
//noinspection ObjectEquality
if (set == templateSet) {
return "index";
}
if (extension.equals(set.getExtension())) {
break;
}
}
}
}
final String name = getName(contentProducer, language);
final String pageSuffix = pageParams.getPageSuffix();
if (!pageSuffix.isEmpty()) {
return name + '_' + pageSuffix;
}
return name;
}
/**
* Build a name for the provided node. This implementation takes the language-dependent name (see
* {@link de.espirit.firstspirit.access.store.IDProvider#getLanguageInfo(Language)}). If this is not set for the
* provided language, the
* {@link de.espirit.firstspirit.access.project.Project#getMasterLanguage() project master language} is used. If
* this is also not set, the {@link de.espirit.firstspirit.access.store.IDProvider#getUid() uid of the node } is
* used. Then leading and trailing chars are stripped and some chars with special meaning in URLs and file names are
* replaced by '-' (see {@link #cleanup(String)}).
*
* @param node Get the name for this node.
* @param language Get the name for this language.
* @return Name part of path for provided node and language.
*/
private String getName(final IDProvider node, final Language language) {
LanguageInfo languageInfo = node.getLanguageInfo(language);
String displayName = languageInfo != null ? languageInfo.getDisplayName() : null;
if (displayName != null) {
final String cleaned = cleanup(displayName);
if ( ! cleaned.isEmpty()) {
return cleaned;
}
}
final Language masterLanguage = node.getProject().getMasterLanguage();
//noinspection ObjectEquality
if (masterLanguage != language) {
languageInfo = node.getLanguageInfo(masterLanguage);
if (languageInfo != null) {
displayName = languageInfo.getDisplayName();
if (displayName != null) {
final String cleaned = cleanup(displayName);
if ( ! cleaned.isEmpty()) {
return cleaned;
}
}
}
}
return cleanup(node.getUid());
}
/**
* Recursive method building a slash-delimited path for the provided folder a it's parent chain. For each folder on the
* chain this methods calls {@link #getName(de.espirit.firstspirit.access.store.IDProvider,de.espirit.firstspirit.access.Language) getName(folder, language)}. For the root folder
* (<tt>{@link IDProvider#getParent() folder.getParent()} == null</tt>) the constructed path is empty. The
* constructed path will be appended to the provided {@link StringBuilder}. The constructed path will start and end
* with a slash.
*
* @param folder folder for which the path from root is collected.
* @param language language, will be forwarded to {@link #getName(IDProvider,Language)} to build the name of each
* path element
* @param templateSet template set, piped through to {@link PathLookup#lookupPath(IDProvider, Language, TemplateSet)}
* - may be {@code null}.
* @param length size estimation for the StringBuilder, will be increased in every call, used to
* {@link StringBuilder#ensureCapacity(int) ensure its capacity} to prevent frequent resizing
* @param collector the builded path is appended to this instance
*/
final void collectPath(final IDProvider folder, final Language language, @Nullable final TemplateSet templateSet, final int length, final StringBuilder collector) {
String name = _pathLookup.lookupPath(folder, language, templateSet);
if (name == null) {
name = getName(folder, language);
collectPath(folder.getParent(), language, templateSet, length + name.length() + 1, collector);
collector.append(name);
collector.append('/');
} else {
final int len = name.length();
if (len > 0) {
collector.ensureCapacity(length + len + 2);
if (name.charAt(0) != '/') {
collector.append('/');
}
collector.append(name);
} else {
collector.ensureCapacity(length + 1);
}
collector.append('/');
}
}
/**
* Helper to determine the extension (e.g. "gif", "pdf") for the given media in the given {@code language} and
* {@code resolution}.<p />
*
* Provided language may be {@code null} for language independent media objects, resolution is null media objects
* of type {@link Media#FILE}.
*
* @param media Media node to get the extension for.
* @param lang Language to get the extension for (is {@code null} if provided media not isn't langage dependent).
* @param resolution Resolution to get the extension for (is {@code null} if provided media is of type {@link Media#FILE}).
* @return File extension.
*/
@Nullable
String getExtension(final Media media, @Nullable final Language lang, @Nullable final Resolution resolution) {
if (media.getType() == Media.FILE) {
final File file = media.getFile(lang);
if (file == null) {
throw new RuntimeException("no file data found for media:\"" + media.getUid() + "\" (id=" + media.getId() + ')');
}
return file.getExtension();
} else {
try {
final Picture picture = media.getPicture(lang);
if (picture == null) {
throw new RuntimeException("no picture data found for media:\"" + media.getUid() + "\" (id=" + media.getId() + ')');
}
final MediaMetaData mediaMetaData = picture.getPictureMetaData(resolution);
if (mediaMetaData == null) {
throw new RuntimeException("no picture data found for media:\"" + media.getUid() + "\" (id=" + media.getId() + ')');
}
return mediaMetaData.getExtension();
} catch (final IOException e) {
throw new IoError(e);
}
}
}
/**
* Matcher for chars (with special meaning in URIs or not valid in windows file names):<ul>
* <li>;</li>
* <li>@</li>
* <li>&</li>
* <li>=</li>
* <li>+</li>
* <li>$</li>
* <li>,</li>
* <li>/</li>
* <li>\</li>
* <li><</li>
* <li>></li>
* <li>:</li>
* <li>*</li>
* <li>|</li>
* <li>#</li>
* <li>?</li>
* <li>"</li>
* <li>whitespace</li>
* </ul>
*
*/
private static final Pattern SPECIAL_CHARS = Pattern.compile("(;|@|&|=|\\+|\\$|,|/|\\\\|<|>|:|\\*|\\||#|\\?|\"|\\s|-)+");
/**
* Strips leading and trailing whitespaces and replaces whitespaces and chars wich are have a special meaning in
* URIs or filenames (e.g. under Windows(TM)) with a single minus character.
*
* @param name String to clean up.
* @return Cleaned string.
*/
final String cleanup(String name) {
name = name.toLowerCase().trim();
final Matcher matcher = SPECIAL_CHARS.matcher(name);
if (matcher.find()) {
String cleaned = matcher.replaceAll("-");
cleaned = replaceUmlauts(cleaned);
if (cleaned.length() == 1) {
return cleaned;
}
if (cleaned.charAt(0) == '-') {
cleaned = cleaned.substring(1);
}
final int length = cleaned.length();
if (cleaned.charAt(length - 1) == '-') {
cleaned = cleaned.substring(0, length - 1);
}
return cleaned;
}
return replaceUmlauts(name);
}
/**
* Replaces German Umlauts. ä is replaced by ae, ö is replaced by oe, ü is replaced by ue and ß is replaced by ss.
*
* @param name String to replace umlauts in
* @return Cleaned string
*/
private String replaceUmlauts(String name){
return name.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss");
}
private static String resolve(Entity entity, final String varName, final Language language, final String defaultLabel) {
String attribute = varName;
final String[] attributes = varName.split("\\.");
if (attributes.length > 1) {
final int lastIndex = attributes.length - 1;
for (int i = 0; i < lastIndex; i++) {
final Object value = entity.getValue(attributes[i]);
if (value instanceof Entity) {
entity = (Entity) value;
} else {
return defaultLabel;
}
}
attribute = attributes[lastIndex];
}
if (entity.getEntityType().getAttribute(attribute) == null) {
attribute = attribute + '_' + language.getAbbreviation();
}
final Object value = entity.getValue(attribute);
if (value == null || "".equals(value)) {
return defaultLabel;
}
return value.toString();
}
}