Search the FirstSpirit Knowledge Base
Hallo zusammen,
ich habe einen eigenen URLCreator geschrieben, indem ich die URLFactory aus dem FS-5 Beispiel minimal abgeändert habe.
Es wird nach wie vor der Display Name für die Namen von Verzeichnissen und Dateien verwendet und ich setze diese lediglich als lowercase und ersetze Umlaute und ß entsprechend.
Anfangs hat das auch wunderbar funktioniert, doch jetzt stelle ich fest, das manche als Startseite deklarierte Seiten nicht mehr automatisch index.html, sondern index-2.html genannt werden. Im Code finde ich aber echt keine Stelle an der das passieren könnte. Genauso ein Verhalten habe ich bei normalen Inhaltsseiten. Hier gibt es in der Struktur eine Seite mit Displayname Bildergalerie, der ja dann bei der Generierung zu einer Datei bildergalerie.html werden sollte. Leider wird seit einiger Zeit immer nur eine Datei mit Namen bildergalerie-3.html generiert.
Ich habe auch schon mehrmal die Funktion gespeicherte URLs zurücksetzen durchgeführt, doch es ändert sich nichts.
Hat hier jemand eine Idee, was wir noch machen könnten?
Grüße,
Stefan
Hallo Stefan,
werf doch mal einen Blick in die Logs des Generierungsauftrages. Wenn die besagte URL nicht exakt durch Eure URLFactory erzeugt wurde, kolidiert diese URL höchstwahrscheinlich mit einem URL-Eintrag den FirstSpirit noch gespeichert hat. In unserem Fall werden die gespeicherten URLs von gelöschten Elementen nicht automatisch durch FS zurückgesetzt (bekannter Bug), im Generierungsauftrag finden sich dann für Elemente welche die gleiche URL wie ein zuvor ein gelöschtes bekommen würden Log Ausgaben die auf einen URL Konflikt hinweisen:
WARN 25.07.2013 18:51:15.981 (de.espirit.firstspirit.generate.path.RegistryUrlFactory): URI conflict detected..
In einem solchem Fall hängt FS allerdings am Ende der URL die durch die eigene URLFactory erzeugt wurde -n an (also z.B. index.html-2), also etwas anders als du es beschrieben hast. Aber vielleicht hilfts ja dennoch
Mit einem Skript zur löschen der von FS gespeicherten URLs gelöschter Element könnte ich bei Bedarf dienen :smileycool:
Gruß,
Hendrik
Hallo Hendrik,
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();
}
}
Hallo Stefan,
ich kann auf den ersten Blick nicht erkennen woran es liegen könnte. Rein theoretisch müsste ja die Methode getName(...) einen falschen Namen liefern... Ich würde einfach mal ein paar Log-Ausgaben einbauen, dann hast du schnell Gewissheit ob es an deinem Code liegt oder ob FS deine erzeugte URL abändert. Ich tippe mal auf ersteres 😉 Wenn klar ist das Eure URLFactory die Fehlerquelle ist kannst du diese auch über deine IDE per remote Debugging analysieren.
Gruß,
Hendrik
Mal ne ganz blöde Frage. Wie richtige ich hier das Logging ein?
Ich nutze hier wie im Entwicklerhandbuch für Komponenten beschrieben die Klasse de.espirit.common.Logging.
Wo wird jetzt aber die Ausgabe geloggt? Habe jetzt mehrere Logging.logDebug() Ausgabe drin, aber ich kann nicht finden, wo diese ausgegeben werden. Der Server steht bei den Protokollierungseinstellungen auf Debug. Im Generierungslog finde ich keine Ausgaben.
Gegenfrage: Erscheinen andere DEBUG Logausgaben, z.B. eines einfachen ScriptTask der ein context.logDebug("Hello World!") enthält ? Falls nein passt an der Logging Konfiguration irgendetwas nicht. Wir nutzen den gleichen FS Logging-Mechanismus und das funktioniert soweit ganz gut.
Hallo,
das Logging sollte eigentlich auf genau diese Weise funktionieren.
Beim schnellen nachstellen gab es das Problem, dass der FirstSpirit Server trotz des Modulupdates, weiterhin die Logik der alten Version zum Erzeugen der URL genommen hat.
Das könnte erklären, warum nachträglich hinzugefügte Logging Ausgaben nicht erscheinen.
Hast du zufällig die Möglichkeit das Modul auf einem Server zu installieren, wo es bisher noch nicht installiert war, um dort zu testen, ob das Logging funktioniert und die Dateinamen weiterhin eine Zahl angehängt bekommen?
Viele Grüße
Rouven
Im Log des Generierungsauftrags finde ich nicht den geringsten Heinweis auf einen Konflikt
Der Konflikt wird auch nur bei der Erzeugung und Überführung in den persistenten Wert protokolliert. Hast du direkt vor der Generierung der Seite die gespeicherten URLs für diese Seite zurückgesetzt?
Hallo,
ich habe genau das gleiche Problem. Hier scheint irgendein Fehler im Beispiel vorzuliegen.
Viele Grüße,
Josef
Hm, also ich habe direkt vor der Generierung due URLs im CMS zurückgesetzt. Das Logfile zeigt aber nichts an. Alles sehr mysteriös.