Hallo zusammen,
wir haben im Rahmen der Einführung von FirstSpirit 5 das mitgelieferte Beispiel zur UrlFactory geringfügig angepasst.
Hier der Quellcode:
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.HashMap;
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 XUrlFactory 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 = cleanup(contentProducer.getFilename());
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|-)+");
private static final Pattern SPECIAL_CHARS = Pattern
.compile("[^A-Za-z0-9.]+");
/**
* 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.trim();
name = name.toLowerCase();
name = replaceMutatedVowels(name);
final Matcher matcher = SPECIAL_CHARS.matcher(name);
if (matcher.find()) {
String cleaned = matcher.replaceAll("-");
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 name;
}
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();
}
private String replaceMutatedVowels(String name) {
return name.replace("\u00FC", "ue").replace("\u00E4", "ae").replace("\u00F6","oe").replace("\u00df","ss");
}
}
Seit dem Update auf FirstSpirit 5.2. können keine neuen Seiten mehr generiert werden - Fehlermeldung:
ERROR 23.03.2016 12:55:49.779 (de.espirit.firstspirit.generate.SiteProduction): generate of id=40007 failed - java.lang.IllegalArgumentException: de.espirit.firstspirit.store.access.sitestore.PageRefImpl
java.lang.IllegalArgumentException: de.espirit.firstspirit.store.access.sitestore.PageRefImpl
at de.espirit.firstspirit.generate.PathLookup$1.lookupPath(PathLookup.java:52)
at de.bgv.web.firstspirit.urlfactory.BGVUrlFactory.getUrl(BGVUrlFactory.java:86)
at de.espirit.firstspirit.generate.path.RegistryUrlFactory.getUrl(RegistryUrlFactory.java:69)
at de.espirit.firstspirit.generate.PluggableUrlCreator.getFilename(PluggableUrlCreator.java:185)
at de.espirit.firstspirit.generate.SiteProduction.getFileHandle(SiteProduction.java:309)
at de.espirit.firstspirit.generate.SiteProduction.generateFiles(SiteProduction.java:220)
at de.espirit.firstspirit.generate.SiteProduction.render(SiteProduction.java:195)
at de.espirit.firstspirit.generate.SiteProduction.render(SiteProduction.java:150)
at de.espirit.firstspirit.generate.SiteProduction.start(SiteProduction.java:120)
at de.espirit.firstspirit.generate.SiteProduction.start(SiteProduction.java:113)
at de.espirit.firstspirit.server.scheduler.GenerateTaskExecutor.run(GenerateTaskExecutor.java:237)
at de.espirit.firstspirit.server.scheduler.ScheduleManagerImpl$TaskCallable.executeLocal(ScheduleManagerImpl.java:2258)
at de.espirit.firstspirit.server.scheduler.ScheduleManagerImpl$TaskCallable.executeLocal(ScheduleManagerImpl.java:2241)
at de.espirit.firstspirit.server.scheduler.ScheduleManagerImpl$TaskCallable.call(ScheduleManagerImpl.java:2164)
at de.espirit.firstspirit.server.ExecutionManagerImpl$ExtendedCallable.call(ExecutionManagerImpl.java:600)
at java.util.concurrent.FutureTask.run(FutureTask.java:262)
at de.espirit.common.util.BoundedExecutorService$RunnableWrapper.run(BoundedExecutorService.java:436)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask.run(FutureTask.java:262)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
at de.espirit.common.util.SuspendableThread.run(SuspendableThread.java:55)
Gibt es eine Möglichkeit das Problem - zumindest temporär - relativ einfach zu lösen, damit die Generierung mit den passenden URLs zumindest mal durchläuft.
Besten Dank und viele Grüße
Daniel