/*
* 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.deltaspike.jsf.api.config.view;
import org.apache.deltaspike.core.api.config.view.metadata.SkipMetaDataMerge;
import org.apache.deltaspike.core.api.config.view.metadata.ViewMetaData;
import org.apache.deltaspike.core.spi.config.view.ConfigPreProcessor;
import org.apache.deltaspike.core.spi.config.view.ViewConfigNode;
import org.apache.deltaspike.core.util.ClassUtils;
import org.apache.deltaspike.jsf.api.config.base.JsfBaseConfig;
import org.apache.deltaspike.jsf.api.literal.ViewLiteral;
import org.apache.deltaspike.jsf.util.NamingConventionUtils;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.Set;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.apache.deltaspike.jsf.api.config.view.View.Extension.XHTML;
import static org.apache.deltaspike.jsf.api.config.view.View.NavigationMode.FORWARD;
import static org.apache.deltaspike.jsf.api.config.view.View.ViewParameterMode.EXCLUDE;
/**
* Optional annotation to specify page specific meta-data
*/
//don't use @Inherited
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@ViewMetaData(preProcessor = View.ViewConfigPreProcessor.class)
public @interface View
{
/**
* Allows to specify a custom base-path for the page represented by the view-config
* @return base-path
*/
@SkipMetaDataMerge
String basePath() default "";
/**
* Allows to specify a custom (file-)name for the page represented by the view-config
*
* @return name of the page
*/
@SkipMetaDataMerge
String name() default "";
/**
* Allows to specify the (file-)extension for the page represented by the view-config
*
* @return extension of the page
*/
String extension() default Extension.DEFAULT;
/**
* Allows to specify navigation-mode which should be used to navigate to the page represented by the view-config
*
* @return navigation-mode which should be used to navigate to the page represented by the view-config
*/
NavigationMode navigation() default NavigationMode.DEFAULT;
/**
* for including view params in jsf2
*
* @return the strategy which should be used by jsf2 for handling view-parameters (for the navigation)
*/
ViewParameterMode viewParams() default ViewParameterMode.DEFAULT;
/**
* Allows to add a custom inline path-builder
* (a custom default implementation can be configured globally via the config mechanism provided by DeltaSpike)
* @return path builder which allows to customize the naming conventions for the base-path
*/
Class<? extends NameBuilder> basePathBuilder() default DefaultBasePathBuilder.class;
/**
* Allows to add a custom inline path-builder
* (a custom default implementation can be configured globally via the config mechanism provided by DeltaSpike)
* @return path builder which allows to customize the naming conventions for the file-name
*/
Class<? extends NameBuilder> fileNameBuilder() default DefaultFileNameBuilder.class;
/**
* Allows to add a custom inline path-builder
* (a custom default implementation can be configured globally via the config mechanism provided by DeltaSpike)
* @return path builder which allows to customize the naming conventions for the file-extension
*/
Class<? extends NameBuilder> extensionBuilder() default DefaultExtensionBuilder.class;
/**
* Extension of the markup file
*/
interface Extension
{
String DEFAULT = "";
String XHTML = "xhtml";
String JSF = "jsf";
String FACES = "faces";
String JSP = "jsp";
}
/**
* Type of the navigation which should be used by the {@link javax.faces.application.NavigationHandler}
*/
enum NavigationMode
{
DEFAULT, FORWARD, REDIRECT
}
/**
* Mode specifies if JSF2 should include view-params
*/
enum ViewParameterMode
{
DEFAULT, INCLUDE, EXCLUDE
}
static class ViewConfigPreProcessor implements ConfigPreProcessor<View>
{
@Override
public View beforeAddToConfig(View view, ViewConfigNode viewConfigNode)
{
validateViewMetaData(view, viewConfigNode);
boolean defaultValueReplaced = false;
View.NavigationMode navigation = view.navigation();
View.ViewParameterMode viewParams = view.viewParams();
/*
* base path
*/
NameBuilder basePathBuilder = getBasePathBuilder(view);
String basePath = basePathBuilder.build(view, viewConfigNode);
if (basePathBuilder.isDefaultValueReplaced())
{
defaultValueReplaced = true;
}
/*
* file name
*/
NameBuilder fileNameBuilder = getFileNameBuilder(view);
String name = fileNameBuilder.build(view, viewConfigNode);
if (fileNameBuilder.isDefaultValueReplaced())
{
defaultValueReplaced = true;
}
/*
* extension
*/
NameBuilder extensionBuilder = getExtensionBuilder(view);
String extension = extensionBuilder.build(view, viewConfigNode);
if (extensionBuilder.isDefaultValueReplaced())
{
defaultValueReplaced = true;
}
/*
* navigation
*/
if (View.NavigationMode.DEFAULT.equals(navigation) || navigation == null)
{
defaultValueReplaced = true;
navigation = FORWARD;
}
if (View.ViewParameterMode.DEFAULT.equals(viewParams) || viewParams == null)
{
defaultValueReplaced = true;
viewParams = EXCLUDE;
}
if (defaultValueReplaced)
{
View result = new ViewLiteral(basePath, name, extension, navigation, viewParams,
view.basePathBuilder(), view.fileNameBuilder(), view.extensionBuilder());
updateNodeMetaData(viewConfigNode, result);
return result;
}
return view;
}
protected void validateViewMetaData(View view, ViewConfigNode viewConfigNode)
{
String basePath = view.basePath();
if (viewConfigNode.getSource().isInterface() && !"".equals(basePath) && basePath != null)
{
throw new IllegalStateException("Using @" + View.class.getName() + "#basePath isn't allowed on" +
" folder-nodes (= interfaces). Use @" + Folder.class.getName() + " for intended folder-nodes" +
" or a class instead of the interface for page-nodes.");
}
}
private void updateNodeMetaData(ViewConfigNode viewConfigNode, View view)
{
Set<Annotation> metaData = viewConfigNode.getMetaData();
Iterator<? extends Annotation> metaDataIterator = metaData.iterator();
while (metaDataIterator.hasNext())
{
if (View.class.equals(metaDataIterator.next().annotationType()))
{
metaDataIterator.remove();
break;
}
}
metaData.add(view);
}
private NameBuilder getBasePathBuilder(View view)
{
NameBuilder basePathBuilder;
if (DefaultBasePathBuilder.class.equals(view.basePathBuilder()))
{
String customDefaultBasePathBuilderClassName =
JsfBaseConfig.ViewConfigCustomization.CUSTOM_DEFAULT_BASE_PATH_BUILDER;
if (customDefaultBasePathBuilderClassName != null)
{
basePathBuilder =
(NameBuilder)ClassUtils.tryToInstantiateClassForName(customDefaultBasePathBuilderClassName);
}
else
{
basePathBuilder = new DefaultBasePathBuilder();
}
}
else
{
basePathBuilder = ClassUtils.tryToInstantiateClass(view.basePathBuilder());
}
return basePathBuilder;
}
private NameBuilder getFileNameBuilder(View view)
{
NameBuilder fileNameBuilder;
if (DefaultFileNameBuilder.class.equals(view.fileNameBuilder()))
{
String customDefaultFileNameBuilderClassName =
JsfBaseConfig.ViewConfigCustomization.CUSTOM_DEFAULT_FILE_NAME_BUILDER;
if (customDefaultFileNameBuilderClassName != null)
{
fileNameBuilder =
(NameBuilder)ClassUtils.tryToInstantiateClassForName(customDefaultFileNameBuilderClassName);
}
else
{
fileNameBuilder = new DefaultFileNameBuilder();
}
}
else
{
fileNameBuilder = ClassUtils.tryToInstantiateClass(view.fileNameBuilder());
}
return fileNameBuilder;
}
private NameBuilder getExtensionBuilder(View view)
{
NameBuilder extensionBuilder;
if (DefaultExtensionBuilder.class.equals(view.extensionBuilder()))
{
String customDefaultExtensionBuilderClassName =
JsfBaseConfig.ViewConfigCustomization.CUSTOM_DEFAULT_EXTENSION_BUILDER;
if (customDefaultExtensionBuilderClassName != null)
{
extensionBuilder =
(NameBuilder)ClassUtils.tryToInstantiateClassForName(customDefaultExtensionBuilderClassName);
}
else
{
extensionBuilder = new DefaultExtensionBuilder();
}
}
else
{
extensionBuilder = ClassUtils.tryToInstantiateClass(view.extensionBuilder());
}
return extensionBuilder;
}
//it's possible that the given source is a folder-node
//e.g. @View(navigation = REDIRECT) specified for a whole folder
private static boolean isView(Class source)
{
return !Modifier.isAbstract(source.getModifiers()) && !Modifier.isInterface(source.getModifiers());
}
}
//TODO discuss if we use a central interface in the spi package
//advantage: can be reused
//disadvantage: a wrong builder can get assigned more easily, show usages will list more
interface NameBuilder
{
String build(View view, ViewConfigNode viewConfigNode);
boolean isDefaultValueReplaced();
}
abstract class AbstractNameBuilder implements NameBuilder
{
protected boolean defaultValueReplaced = false;
public boolean isDefaultValueReplaced()
{
return defaultValueReplaced;
}
}
class DefaultBasePathBuilder extends AbstractNameBuilder
{
@Override
public String build(View view, ViewConfigNode viewConfigNode)
{
String basePath = view.basePath();
Class source = viewConfigNode.getSource();
if (("".equals(basePath) || basePath == null) &&
ViewConfigPreProcessor.isView(source) /*only calc the path for real pages*/)
{
this.defaultValueReplaced = true;
basePath = NamingConventionUtils.toPath(viewConfigNode.getParent());
}
if (basePath != null && basePath.startsWith("."))
{
basePath = NamingConventionUtils.toPath(viewConfigNode.getParent()) + basePath.substring(1);
this.defaultValueReplaced = true;
}
if (basePath != null && !basePath.startsWith(".") && !basePath.startsWith("/"))
{
basePath = NamingConventionUtils.toPath(viewConfigNode.getParent()) + basePath;
this.defaultValueReplaced = true;
}
if (basePath != null && !basePath.endsWith("/"))
{
basePath = basePath + "/";
this.defaultValueReplaced = true;
}
if (basePath != null && basePath.contains("//"))
{
basePath = basePath.replace("//", "/");
this.defaultValueReplaced = true;
}
return basePath;
}
}
class DefaultFileNameBuilder extends AbstractNameBuilder
{
@Override
public String build(View view, ViewConfigNode viewConfigNode)
{
String name = view.name();
Class source = viewConfigNode.getSource();
if (("".equals(name) || name == null) &&
ViewConfigPreProcessor.isView(source) /*only calc the path for real pages*/)
{
this.defaultValueReplaced = true;
String className = viewConfigNode.getSource().getSimpleName();
name = className.substring(0, 1).toLowerCase() + className.substring(1);
}
return name;
}
}
class DefaultExtensionBuilder extends AbstractNameBuilder
{
@Override
public String build(View view, ViewConfigNode viewConfigNode)
{
String extension = view.extension();
if (View.Extension.DEFAULT.equals(extension) || extension == null)
{
defaultValueReplaced = true;
extension = XHTML;
}
return extension;
}
}
}