Index: main/plugin-cms/sitemap.xmap
===================================================================
--- main/plugin-cms/sitemap.xmap	(revision 25394)
+++ main/plugin-cms/sitemap.xmap	(working copy)
@@ -25,6 +25,7 @@
 			<map:generator name="referencing-contents" src="org.ametys.cms.content.ReferencingContentsGenerator" logger="org.ametys.cms.content.ReferencingContentsGenerator"/>
             <map:generator name="sql-search" src="org.ametys.cms.repository.SQLSearchGenerator" logger="org.ametys.cms.repository.SearchGenerator"/>
             <map:generator name="search" src="org.ametys.cms.repository.SearchGenerator" logger="org.ametys.cms.repository.SearchGenerator"/>
+            <map:generator name="neo-search" src="org.ametys.cms.search.generators.SearchGenerator" logger="org.ametys.cms.search"/>
             <map:generator name="search-columns" src="org.ametys.cms.repository.SearchColumnsGenerator"/>
             
             <map:generator name="simple-contents" src="org.ametys.cms.repository.SimpleContentsGenerator"/>
@@ -88,6 +89,8 @@
             <map:action name="set-auto-backup" src="org.ametys.cms.content.autosave.SetContentAutoBackup" logger="org.ametys.cms.content.autosave.SetContentAutoBackup"/>
             <map:action name="delete-auto-backup" src="org.ametys.cms.content.autosave.DeleteContentAutoBackup" logger="org.ametys.cms.content.autosave.DeleteContentAutoBackup"/>
             
+             <map:action name="search-model" src="org.ametys.cms.search.actions.GetSearchModelAction"/>
+             
             <map:action name="select-workspace" src="org.ametys.cms.repository.SelectWorkspaceAction" logger="org.ametys.cms.repository.SelectWorkspaceAction"/>
             
             <map:action name="import-simple-contents" src="org.ametys.cms.repository.ImportSimpleContentsAction"/>
@@ -476,6 +479,15 @@
             <!-- +
                  | CONTENT SEARCH
                  + -->
+                 
+           <map:match pattern="neo-search/list.xml">
+                <map:generate type="neo-search"/>
+                <map:transform type="i18n">
+                    <map:parameter name="locale" value="{locale:locale}"/>
+                </map:transform>
+                <map:serialize type="xml"/>
+            </map:match>
+                 
            <map:match pattern="sql-search/list.xml">
                 <map:generate type="sql-search"/>
                 <map:transform type="i18n">
@@ -497,6 +509,12 @@
                 <map:serialize type="xml"/>
             </map:match>
             
+            <map:match pattern="search/model.json">
+                <map:act type="search-model">
+                    <map:read type="json"/>
+                </map:act>
+            </map:match>
+            
             <map:match pattern="search/export.xls">
                 <map:act type="set-header"> 
                     <map:parameter name="Content-Disposition" value="attachment"/>
Index: main/plugin-cms/src/org/ametys/cms/search/SearchModelExtensionPoint.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/SearchModelExtensionPoint.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/SearchModelExtensionPoint.java	(revision 0)
@@ -0,0 +1,124 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Set;
+
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.DefaultConfiguration;
+import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
+import org.apache.cocoon.Constants;
+import org.apache.cocoon.environment.Context;
+import org.apache.commons.io.IOUtils;
+
+import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
+
+/**
+ * Extension point for {@link SearchModel}s.
+ */
+public class SearchModelExtensionPoint extends AbstractThreadSafeComponentExtensionPoint<SearchModel>
+{
+    /** The Avalon role */
+    public static final String ROLE = SearchModelExtensionPoint.class.getName();
+    
+    private boolean _initialized;
+    
+    @Override
+    public SearchModel getExtension(String id)
+    {
+        _delayedInitializeExtensions();
+        return super.getExtension(id);
+    }
+    
+    @Override
+    public Set<String> getExtensionsIds()
+    {
+        _delayedInitializeExtensions();
+        return super.getExtensionsIds();
+    }
+    
+    @Override
+    public boolean hasExtension(String id)
+    {
+        _delayedInitializeExtensions();
+        return super.hasExtension(id);
+    }
+    
+    @Override
+    public void initializeExtensions() throws Exception
+    {
+        // Nothing
+    }
+    
+    private void _delayedInitializeExtensions()
+    {
+        if (!_initialized)
+        {
+            try
+            {
+                Context ctx = (Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
+                
+                File root = new File(ctx.getRealPath("WEB-INF/param/search"));
+                
+                if (root.exists())
+                {
+                    File[] files = root.listFiles();
+                    
+                    for (File modelFile : files)
+                    {
+                        String fileName = modelFile.getName();
+                        
+                        InputStream is = null;
+                        try
+                        {
+                            is = new FileInputStream(modelFile);
+                            
+                            String id = "search-model." + fileName.substring(0, fileName.lastIndexOf('.'));
+                            
+                            if (super.hasExtension(id))
+                            {
+                                throw new IllegalArgumentException("The search model of id " + id + " at " + modelFile.getAbsolutePath() + " is already declared.");
+                            }
+                            
+                            DefaultConfiguration conf = new DefaultConfiguration("extension");
+                            
+                            Configuration c = new DefaultConfigurationBuilder(true).build(is);
+                            
+                            conf.setAttribute("id", id);
+                            conf.addChild(c);
+                            
+                            addComponent("unknown", "unknown", id, StaticSearchModel.class, conf);
+                        }
+                        finally
+                        {
+                            IOUtils.closeQuietly(is);
+                        }
+                    }
+                }
+                
+                super.initializeExtensions();
+                _initialized = true;
+            }
+            catch (Exception e)
+            {
+                getLogger().error("Unable to initialized search models", e);
+            }
+        }
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/SearchModel.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/SearchModel.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/SearchModel.java	(revision 0)
@@ -0,0 +1,839 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.activity.Disposable;
+import org.apache.avalon.framework.configuration.Configurable;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.configuration.DefaultConfiguration;
+import org.apache.avalon.framework.context.Context;
+import org.apache.avalon.framework.context.ContextException;
+import org.apache.avalon.framework.context.Contextualizable;
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.ContentTypeEnumerator;
+import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.MetadataDefinition;
+import org.ametys.cms.contenttype.MetadataType;
+import org.ametys.cms.languages.LanguageEnumerator;
+import org.ametys.cms.repository.LanguageExpression;
+import org.ametys.cms.repository.WorkflowStepExpression;
+import org.ametys.cms.repository.comment.CommentExpression;
+import org.ametys.cms.workflow.DefaultWorkflowStepEnumerator;
+import org.ametys.plugins.repository.query.expression.AndExpression;
+import org.ametys.plugins.repository.query.expression.BooleanExpression;
+import org.ametys.plugins.repository.query.expression.DateExpression;
+import org.ametys.plugins.repository.query.expression.DoubleExpression;
+import org.ametys.plugins.repository.query.expression.Expression;
+import org.ametys.plugins.repository.query.expression.Expression.Operator;
+import org.ametys.plugins.repository.query.expression.FullTextExpression;
+import org.ametys.plugins.repository.query.expression.LongExpression;
+import org.ametys.plugins.repository.query.expression.OrExpression;
+import org.ametys.plugins.repository.query.expression.StringExpression;
+import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
+import org.ametys.runtime.util.I18nizableText;
+import org.ametys.runtime.util.parameter.Enumerator;
+import org.ametys.runtime.util.parameter.ParameterHelper;
+import org.ametys.runtime.util.parameter.ParameterHelper.ParameterType;
+import org.ametys.runtime.util.parameter.StaticEnumerator;
+import org.ametys.runtime.util.parameter.Validator;
+
+/**
+ * This abstract class represents a search model
+ *
+ */
+public abstract class SearchModel extends AbstractLogEnabled implements Serviceable, Contextualizable, Configurable, Disposable
+{
+    /** The valid system properties */
+    public static final String[] SYSTEM_PROPERTY_NAMES = {"creator", "contributor", "lastModified", "comments", "workflowStep", "contentLanguage", "contentType", "fulltext"};
+    /** Prefix for id of metadata search criteria */
+    public static final String SEARCH_CRITERIA_METADATA_PREFIX = "metadata-";
+    /** Prefix for id of system property search criteria */
+    public static final String SEARCH_CRITERIA_SYSTEM_PREFIX = "property-";
+    
+    /** The content type extension point */
+    protected ContentTypeExtensionPoint _cTypeEP;
+    /** ComponentManager for {@link Validator}s. */
+    protected ThreadSafeComponentManager<Validator> _validatorManager;
+    /** ComponentManager for {@link Enumerator}s. */
+    protected ThreadSafeComponentManager<Enumerator> _enumeratorManager;
+    /** The service manager */
+    protected ServiceManager _manager;
+    /** The context. */
+    protected Context _context;
+    
+    boolean _initialized;
+    
+    @Override
+    public void contextualize(Context context) throws ContextException
+    {
+        _context = context;
+    }
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
+        _manager = manager;
+    }
+    
+    @Override
+    public void dispose()
+    {
+        _validatorManager.dispose();
+        _validatorManager = null;
+        _enumeratorManager.dispose();
+        _enumeratorManager = null;
+    }
+    
+    @Override
+    public void configure(Configuration configuration) throws ConfigurationException
+    {
+        try
+        {
+            _validatorManager = new ThreadSafeComponentManager<Validator>();
+            _validatorManager.enableLogging(getLogger());
+            _validatorManager.contextualize(_context);
+            _validatorManager.service(_manager);
+            
+            _enumeratorManager = new ThreadSafeComponentManager<Enumerator>();
+            _enumeratorManager.enableLogging(getLogger());
+            _enumeratorManager.contextualize(_context);
+            _enumeratorManager.service(_manager);
+            
+            try
+            {
+                DefaultConfiguration conf = new DefaultConfiguration("criteria");
+                
+                DefaultConfiguration enumConf = new DefaultConfiguration("enumeration");
+                
+                DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
+                customEnumerator.setAttribute("class", DefaultWorkflowStepEnumerator.class.getName());
+                conf.addChild(customEnumerator);
+                
+                DefaultConfiguration excludeConf = new DefaultConfiguration("exclude-workflow-steps");
+                DefaultConfiguration stepId = new DefaultConfiguration("id");
+                stepId.setValue(9999);
+                excludeConf.addChild(stepId);
+                conf.addChild(excludeConf);
+                
+                conf.addChild(enumConf);
+                
+                _enumeratorManager.addComponent("cms", null, DefaultWorkflowStepEnumerator.class.getName(), DefaultWorkflowStepEnumerator.class, conf);
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("Unable to instantiate enumerator for class: " + DefaultWorkflowStepEnumerator.class.getName(), e);
+            }
+            
+            try
+            {
+                DefaultConfiguration conf = new DefaultConfiguration("criteria");
+                
+                DefaultConfiguration enumConf = new DefaultConfiguration("enumeration");
+                
+                DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
+                customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName());
+                
+                List<String> contentTypes = getContentTypes();
+                if (contentTypes.size() > 0)
+                {
+                    DefaultConfiguration cTypeConf = new DefaultConfiguration("content-types");
+                    cTypeConf.setValue(StringUtils.join(contentTypes, ","));
+                    customEnumerator.addChild(cTypeConf);
+                    
+                    DefaultConfiguration allOptionConf = new DefaultConfiguration("all-option");
+                    allOptionConf.setValue("concat");
+                    customEnumerator.addChild(allOptionConf);
+                }
+                
+                enumConf.addChild(customEnumerator);
+                conf.addChild(enumConf);
+                
+                _enumeratorManager.addComponent("cms", null, ContentTypeEnumerator.class.getName(), ContentTypeEnumerator.class, conf);
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("Unable to instantiate enumerator for class: " + ContentTypeEnumerator.class.getName(), e);
+            }
+            
+            try
+            {
+                _enumeratorManager.addComponent("cms", null, LanguageEnumerator.class.getName(), LanguageEnumerator.class, new DefaultConfiguration("enumeration"));
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("Unable to instantiate enumerator for class: " + LanguageEnumerator.class.getName(), e);
+            }
+            
+            _enumeratorManager.initialize();
+            _validatorManager.initialize();
+        }
+        catch (Exception e)
+        {
+            throw new ConfigurationException("Unable to create local component managers", configuration, e);
+        }
+    }    
+    
+    /**
+     * Get the list of content types
+     * @return The list of content types
+     */
+    public abstract List<String> getContentTypes();
+    
+    /**
+     * Get the URL for search
+     * @return the URL for search
+     */
+    public abstract String getSearchUrl();
+    
+    /**
+     * Get the plugin name for search
+     * @return the plugin name for search
+     */
+    public abstract String getSearchUrlPlugin();
+    
+    /**
+     * Get the URL for CVS export of results
+     * @return the URL for CVS export
+     */
+    public abstract String getExportCSVUrl();
+    
+    /**
+     * Get the plugin name for CVS export of results
+     * @return the plugin name for CVS export
+     */
+    public abstract String getExportCSVUrlPlugin();
+    
+    /**
+     * Get the URL for XML export of results
+     * @return the URL for XML export
+     */
+    public abstract String getExportXMLUrl();
+    
+    /**
+     * Get the plugin name for XML export of results
+     * @return the plugin name for XML export
+     */
+    public abstract String getExportXMLUrlPlugin();
+    
+    /**
+     * Get the URL for print results
+     * @return the URL for print results
+     */
+    public abstract String getPrintUrl();
+    
+    /**
+     * Get the plugin name for print results
+     * @return the plugin name for print results
+     */
+    public abstract String getPrintUrlPlugin();
+    
+    /**
+     * Get the list of search criteria in simple mode
+     * @return the list of search criteria in simple mode
+     */
+    public abstract Map<String, SearchCriteria> getCriteria();
+    
+    /**
+     * Get a simple search criteria by its id
+     * @param id The search criteria id
+     * @return the criteria or <code>null</code> if not found
+     */
+    public SearchCriteria getCriteria (String id)
+    {
+        Map<String, SearchCriteria> criteria = getCriteria();
+        if (criteria.containsKey(id))
+        {
+            return criteria.get(id);
+        }
+        return null;
+    }
+    
+    /**
+     * Get the list of search criteria in advanced mode
+     * @return the list of search criteria in advanced mode
+     */
+    public abstract Map<String, SearchCriteria> getAdvancedCriteria();
+
+    /**
+     * Get the query to execute in simple mode
+     * @param values The search criteria values
+     * @return the query
+     */
+    public String createQuery(Map<String, Object> values)
+    {
+        List<Expression> expressions = new ArrayList<Expression>();
+        
+        Expression cTypeExpr = createContentTypeExpressions(values);
+        if (cTypeExpr != null)
+        {
+            expressions.add(cTypeExpr);
+        }
+        
+        expressions.addAll(getCriteriaExpressions(values));
+        
+        // TODO sortCriteria
+        
+        return org.ametys.plugins.repository.query.QueryHelper.getXPathQuery(null, "ametys:content", getAndExpression(expressions), null);
+    }
+    
+    /**
+     * Get the query to execute in SQL mode
+     * @param sqlQuery The sql query
+     * @return the query
+     */
+    public abstract String createSqlQuery (String sqlQuery);
+    
+    /**
+     * Get the column for results
+     * @return the column for results
+     */
+    public abstract List<SearchColumn> getResultColumns();
+    
+    /**
+     * Get the common ancestor of content types concerned by search
+     * @return The common ancestor or <code>null</code> if there is no common ancestor
+     */
+    public ContentType getCommonContentTypeAncestor ()
+    {
+        List<String> cTypes = getContentTypes();
+        if (cTypes.isEmpty())
+        {
+            return null;
+        }
+        
+        if (cTypes.size() == 1)
+        {
+            return _cTypeEP.getExtension(cTypes.get(0));
+        }
+        
+        // FIXME Get the common ancestor
+        return null;
+    }
+    
+    /**
+     * Get the {@link MetadataDefinition} of a metadata representing by its path
+     * @param cType The initial content type
+     * @param metadataPath The path of the metadata separated by '/'
+     * @return The metadata definition or <code>null</code> if the metadata does not exist.
+     */
+    public MetadataDefinition getMetadataDefinition (ContentType cType, String metadataPath)
+    {
+        if (cType == null)
+        {
+            if ("title".equals(metadataPath))
+            {
+                String cTypeId = _cTypeEP.getExtensionsIds().iterator().next();
+                ContentType firstCType = _cTypeEP.getExtension(cTypeId);
+                return firstCType.getMetadataDefinition(metadataPath);
+            }
+            
+            return null;
+        }
+        
+        String[] path = metadataPath.split("/");
+        
+        MetadataDefinition metadataDefinition = null;
+        for (int i = 0; i < path.length; i++)
+        {
+            // TODO if metadata est de type CONTENT ou CONTENT_REF 
+            metadataDefinition = metadataDefinition != null ? metadataDefinition.getMetadataDefinition(path[i]) : cType.getMetadataDefinition(path[i]);
+            if (metadataDefinition == null)
+            {
+                return null;
+            }
+        }
+        
+        return metadataDefinition;
+    }
+    
+    /**
+     * Determines if the medatata path is a valid path
+     * @param cType The common content type ancestor
+     * @param metadataPath The metadata path
+     * @return true if the path is the valid metadata path
+     */
+    public boolean isMetadataExist (ContentType cType, String metadataPath)
+    {
+        return getMetadataDefinition(cType, metadataPath) != null;
+    }
+    
+    /**
+     * Determines if the property is a valid system property
+     * @param propertyName The property
+     * @return <code>true</code> if the property is valid
+     */
+    public boolean isValidSystemProperty (String propertyName)
+    {
+        return ArrayUtils.contains(SYSTEM_PROPERTY_NAMES, propertyName);
+    }
+    
+    /**
+     * Get the system search criteria
+     * @param propertyName The system property name
+     * @return The search criteria or <code>null</code> if the property is not a valid system property.
+     */
+    public SystemSearchCriteria getSystemSearchCriteria (String propertyName)
+    {
+        if (!isValidSystemProperty(propertyName))
+        {
+            return null;
+        }
+        
+        SystemSearchCriteria systemSC = new SystemSearchCriteria();
+        systemSC.setPropertyName(propertyName);
+        systemSC.setId(propertyName);
+        
+        if (propertyName.equals("creator"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CREATOR"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CREATOR"));
+            systemSC.setType(MetadataType.USER);
+        }
+        else if (propertyName.equals("contributor"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_AUTHOR"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_AUTHOR"));
+            systemSC.setType(MetadataType.USER);
+        }
+        else if (propertyName.equals("workflowStep"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_WORKFLOW_STEP"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_WORKFLOW_STEP"));
+            systemSC.setType(MetadataType.LONG);
+            try
+            {
+                systemSC.setEnumerator(_enumeratorManager.lookup(DefaultWorkflowStepEnumerator.class.getName()));
+            }
+            catch (Exception e)
+            {
+                getLogger().error("Unable to lookup enumerator role: '" + DefaultWorkflowStepEnumerator.class.getName() + "'", e);
+            }
+        }
+        else if (propertyName.equals("lastModified"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_LASTMODIFIED"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_LASTMODIFIED"));
+            systemSC.setType(MetadataType.DATE);
+        }
+        else if (propertyName.equals("comments"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_COMMENTS"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_COMMENTS"));
+            systemSC.setType(MetadataType.STRING);
+            
+            StaticEnumerator staticEnumerator = new StaticEnumerator();
+            staticEnumerator.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_SEARCH_ALL"), "with-comments");
+            staticEnumerator.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_SEARCH_VALIDATED"), "with-validated-comments");
+            staticEnumerator.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_SEARCH_NOTVALIDATED"), "with-nonvalidated-comments");
+            systemSC.setEnumerator(staticEnumerator);
+        }
+        else if (propertyName.equals("contentLanguage"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_LANGUAGE"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_LANGUAGE"));
+            systemSC.setType(MetadataType.STRING);
+            try
+            {
+                systemSC.setEnumerator(_enumeratorManager.lookup(LanguageEnumerator.class.getName()));
+            }
+            catch (Exception e)
+            {
+                getLogger().error("Unable to lookup enumerator role: '" + DefaultWorkflowStepEnumerator.class.getName() + "'", e);
+            }
+        }
+        else if (propertyName.equals("contentType"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CONTENT_TYPE"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CONTENT_TYPE"));
+            systemSC.setType(MetadataType.STRING);
+            try
+            {
+                systemSC.setEnumerator(_enumeratorManager.lookup(ContentTypeEnumerator.class.getName()));
+            }
+            catch (Exception e)
+            {
+                getLogger().error("Unable to lookup enumerator role: '" + ContentTypeEnumerator.class.getName() + "'", e);
+            }
+        }
+        if (propertyName.equals("fulltext"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_FULLTEXT"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_FULLTEXT"));
+            systemSC.setType(MetadataType.STRING);
+        }
+        
+        return systemSC;
+    }
+    
+    /**
+     * Get the system result columns
+     * @param propertyName The system property name
+     * @return The result column or <code>null</code> if the property is not a valid system property.
+     */
+    public SearchColumn getSystemSearchColumn (String propertyName)
+    {
+        if (!isValidSystemProperty(propertyName))
+        {
+            return null;
+        }
+        
+        SearchColumn column = new SearchColumn();
+        column.setId(propertyName);
+        column.setEditable(false);
+        column.setSortable(true);
+        column.setMapping(propertyName);
+        
+        if (propertyName.equals("creator"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CREATOR"));
+            column.setType(MetadataType.USER);
+            column.setRenderer("Ametys.cms.content.EditContentsGrid.renderDisplay");
+            column.setWidth(150);
+        }
+        else if (propertyName.equals("contributor"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_AUTHOR"));
+            column.setType(MetadataType.USER);
+            column.setRenderer("Ametys.cms.content.EditContentsGrid.renderDisplay");
+            column.setWidth(150);
+        }
+        else if (propertyName.equals("workflowStep"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_WORKFLOW_STEP"));
+            column.setType(MetadataType.STRING);
+            column.setWidth(40);
+            column.setRenderer("Ametys.cms.content.EditContentsGrid.renderWorkflowStep");
+            column.setMapping("workflow-step");
+        }
+        else if (propertyName.equals("lastModified"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_LASTMODIFIED"));
+            column.setType(MetadataType.DATE);
+            column.setWidth(100);
+            column.setRenderer("Ext.util.Format.dateRenderer(Ext.Date.patterns.FriendlyDateTime)");
+        }
+        else if (propertyName.equals("comments"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_COMMENTS"));
+            column.setType(MetadataType.STRING);
+            column.setWidth(40);
+            column.setRenderer("Ametys.cms.content.EditContentsGrid.renderBooleanIcon");
+        }
+        else if (propertyName.equals("contentLanguage"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_LANGUAGE"));
+            column.setType(MetadataType.STRING);
+            column.setWidth(40);
+            column.setMapping("@language");
+            column.setRenderer("Ametys.cms.content.EditContentsGrid.renderLanguage");
+        }
+        else if (propertyName.equals("contentType"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CONTENT_TYPE"));
+            column.setType(MetadataType.STRING);
+            column.setWidth(150);
+            column.setMapping("content-type");
+        }
+        return column;
+    }
+    
+    /**
+     * Create expression on content type
+     * @param values The submitted values
+     * @return The content type expression or null if no
+     */
+    @SuppressWarnings("unchecked")
+    protected Expression createContentTypeExpressions(Map<String, Object> values)
+    {
+        List<String> cTypes = getContentTypes();
+        
+        Object cTypeParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "contentType-eq");
+        
+        if (cTypeParam instanceof String && StringUtils.isNotEmpty((String) cTypeParam))
+        {
+            return _cTypeEP.createHierarchicalCTExpression((String) cTypeParam);
+        }
+        else if (cTypeParam != null && cTypeParam instanceof List<?>)
+        {
+            cTypes = (List<String>) cTypeParam;
+            return _cTypeEP.createHierarchicalCTExpression(cTypes.toArray(new String[cTypes.size()]));
+        }
+        else if (cTypes != null && cTypes.size() > 0)
+        {
+            return _cTypeEP.createHierarchicalCTExpression(cTypes.toArray(new String[cTypes.size()]));
+        }
+        else
+        {
+            return null;
+        }
+    }
+    
+    /**
+     * Returns the expression to execute
+     * @param values The submitted values
+     * @return The expression to execute
+     */
+    protected List<Expression> getCriteriaExpressions(Map<String, Object> values)
+    {
+        List<Expression> expressions = new ArrayList<Expression>();
+        
+        for (String id : values.keySet())
+        {
+            SearchCriteria criteria = getCriteria(id);
+            if (criteria != null)
+            {
+                String value = (String) values.get(id);
+                if (criteria instanceof MetadataSearchCriteria)
+                {
+                    Expression metadataExpr = getMetadataExpression((MetadataSearchCriteria) criteria, value);
+                    if (metadataExpr != null)
+                    {
+                        expressions.add(metadataExpr);
+                    }
+                }
+                else if (criteria instanceof SystemSearchCriteria)
+                {
+                    Expression sysExpr = getSystemPropertyExpression((SystemSearchCriteria) criteria, value);
+                    if (sysExpr != null)
+                    {
+                        expressions.add(sysExpr);
+                    }
+                }
+            }
+        }
+        
+        return expressions;
+    }
+    
+    /**
+     * Create a comparison's test for a metadata
+     * @param criteria The search criteria
+     * @param value The value. Can be null.
+     * @return The expression that test the comparison of the metadata with the value
+     */
+    protected Expression getMetadataExpression(MetadataSearchCriteria criteria, String value)
+    {
+        if (StringUtils.isEmpty(value))
+        {
+            return null;
+        }
+        
+        switch (criteria.getType())
+        {
+            case DATE:
+            case DATETIME:
+                Date dateValue = (Date) ParameterHelper.castValue(value, ParameterType.DATE);
+                
+                GregorianCalendar calendar = new GregorianCalendar();
+                calendar.setTime(dateValue);
+                calendar.set(Calendar.HOUR, 0);
+                calendar.set(Calendar.MINUTE, 0);
+                calendar.set(Calendar.MILLISECOND, 0);
+
+                if (criteria.getOperator().equals(Operator.LE))
+                {
+                    calendar.add(Calendar.DAY_OF_YEAR, 1);
+                    return new DateExpression(criteria.getMetadataPath(), Operator.LT, calendar.getTime());
+                }
+
+                return new DateExpression(criteria.getMetadataPath(), criteria.getOperator(), calendar.getTime());
+                
+            case LONG:
+                Long longValue = (Long) ParameterHelper.castValue(value, ParameterType.LONG);
+                return new LongExpression(criteria.getMetadataPath(), criteria.getOperator(), longValue);
+                
+            case DOUBLE:
+                Double doubleValue = (Double) ParameterHelper.castValue(value, ParameterType.DOUBLE);
+                return new DoubleExpression(criteria.getMetadataPath(), criteria.getOperator(), doubleValue);
+                
+            case BOOLEAN:
+                Boolean boolValue = (Boolean) ParameterHelper.castValue(value, ParameterType.BOOLEAN);
+                return new BooleanExpression(criteria.getMetadataPath(), boolValue);
+            
+            case STRING:
+                String stringValue = (String) ParameterHelper.castValue(value, ParameterType.STRING);
+                if (criteria.getOperator().equals(Operator.WD))
+                {
+                    String[] wildcardValues = StringUtils.isEmpty(stringValue) ? new String[0] :  value.split("\\s");
+                    
+                    List<Expression> expressions = new ArrayList<Expression>();
+                    for (String wildcardValue : wildcardValues)
+                    {
+                        expressions.add(new StringExpression(criteria.getMetadataPath(), criteria.getOperator(), wildcardValue, false, true));
+                    }
+                    
+                    return getAndExpression(expressions);
+                }
+                else
+                {
+                    return new StringExpression(criteria.getMetadataPath(), criteria.getOperator(), stringValue);
+                }
+            default:
+                return null;
+        }
+    }
+    
+    /**
+     * Create a comparison's test for a system property
+     * @param criteria The search criteria
+     * @param value The value. Can be null.
+     * @return The expression that test the comparison of the metadata with the value
+     */
+    protected Expression getSystemPropertyExpression (SystemSearchCriteria criteria, String value)
+    {
+        if (StringUtils.isEmpty(value))
+        {
+            return null;
+        }
+        
+        String propertyName = criteria.getPropertyName();
+        
+        if ("workflowStep".equals(propertyName))
+        {
+            int stepId = Integer.parseInt(value);
+            if (stepId != 0)
+            {
+                return new WorkflowStepExpression(Operator.EQ, stepId);
+            }
+        }
+        else if ("comments".equals(propertyName))
+        {
+            if ("with-comments".equals(value))
+            {
+                return new CommentExpression(true, true);
+            }
+            else if ("with-validated-comments".equals(value))
+            {
+                return new CommentExpression(true, false);
+            }
+            else // if ("with-nonvalidated-comments".equals(comments))
+            {
+                return new CommentExpression(false, true);
+            }
+        }
+        else if ("contentLanguage".equals(propertyName))
+        {
+            return new LanguageExpression(Operator.EQ, value);
+        }
+        else if ("contributor".equals(propertyName) || "creator".equals(propertyName))
+        {
+            return new StringExpression(propertyName, criteria.getOperator(), value);
+        }
+        else if ("lastModified".equals(propertyName))
+        {
+            Date dateValue = (Date) ParameterHelper.castValue(value, ParameterType.DATE);
+            
+            GregorianCalendar calendar = new GregorianCalendar();
+            calendar.setTime(dateValue);
+            calendar.set(Calendar.HOUR, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+
+            if (criteria.getOperator().equals(Operator.LE))
+            {
+                calendar.add(Calendar.DAY_OF_YEAR, 1);
+                return new DateExpression(propertyName, Operator.LT, calendar.getTime());
+            }
+
+            return new DateExpression(propertyName, criteria.getOperator(), calendar.getTime());
+        }
+        else if ("fulltext".equals(propertyName))
+        {
+            return new FullTextExpression(value);
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Create a comparison's test for a metadata of type <code>Date</code>
+     * @param metadataName The id of the metadata to test
+     * @param testOperator The comparator to use between TEST_Operator_* constants.
+     * @param value The value to test
+     * @return The expression that test the comparison of the metadata with the value using the testOperator
+     */
+    protected Expression createExpression(String metadataName, Date value, Operator testOperator)
+    {
+        GregorianCalendar calendar = new GregorianCalendar();
+        calendar.setTime(value);
+        calendar.set(Calendar.HOUR, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+
+        if (testOperator.equals(Operator.LE))
+        {
+            calendar.add(Calendar.DAY_OF_YEAR, 1);
+            return new DateExpression(metadataName, Operator.LT, calendar.getTime());
+        }
+
+        return new DateExpression(metadataName, testOperator, calendar.getTime());
+    }
+
+    
+    /**
+     * Get an AND expression on an expression collection.
+     * @param expressions the expression collection to include in the AND.
+     * @return an AND expression or null if the collection is empty.
+     */
+    protected Expression getAndExpression(Collection<Expression> expressions)
+    {
+        Expression expression = null;
+        
+        if (!expressions.isEmpty())
+        {
+            Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]);
+            expression = new AndExpression(exprArr);
+        }
+        
+        return expression;
+    }
+    
+    /**
+     * Get an OR expression on an expression collection.
+     * @param expressions the expression collection to include in the OR.
+     * @return an OR expression or null if the collection is empty.
+     */
+    protected Expression getOrExpression(Collection<Expression> expressions)
+    {
+        Expression expression = null;
+        
+        if (!expressions.isEmpty())
+        {
+            Expression[] exprArr = expressions.toArray(new Expression[expressions.size()]);
+            expression = new OrExpression(exprArr);
+        }
+        
+        return expression;
+    }
+    
+}
Index: main/plugin-cms/src/org/ametys/cms/search/StaticSearchModel.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/StaticSearchModel.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/StaticSearchModel.java	(revision 0)
@@ -0,0 +1,410 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.cms.contenttype.AbstractMetadataSetElement;
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.MetadataDefinition;
+import org.ametys.cms.contenttype.MetadataDefinitionReference;
+import org.ametys.cms.contenttype.MetadataSet;
+import org.ametys.plugins.repository.query.expression.Expression.Operator;
+import org.ametys.runtime.util.I18nizableText;
+import org.ametys.runtime.util.parameter.DefaultValidator;
+import org.ametys.runtime.util.parameter.Validator;
+
+/**
+ * Static implementation of a {@link SearchModel}
+ */
+public class StaticSearchModel extends SearchModel
+{
+    private List<String> _cTypes;
+    private String _searchUrl;
+    private String _searchUrlPlugin;
+    private String _exportCSVUrl;
+    private String _exportCSVUrlPlugin;
+    private String _exportXMLUrl;
+    private String _exportXMLUrlPlugin;
+    private String _printUrl;
+    private String _printUrlPlugin;
+ 
+    private Map<String, SearchCriteria> _searchCriteria;
+    private Map<String, SearchCriteria> _advancedSearchCriteria;
+    private List<SearchColumn> _columns;
+    
+    @Override
+    public void configure(Configuration configuration) throws ConfigurationException
+    {
+        Configuration searchConfig = configuration.getChild("SearchModel");
+        _cTypes = _configureContentTypes(searchConfig.getChild("content-types"));
+        
+        super.configure(configuration);
+        
+        _configureSearchUrl(searchConfig);
+        _configureExportCSVUrl(searchConfig);
+        _configureExportXMLUrl(searchConfig);
+        _configurePrintUrl(searchConfig);
+        
+        _searchCriteria = _configureCriteria(searchConfig.getChild("simple-search-criteria"));
+        _advancedSearchCriteria = _configureCriteria(searchConfig.getChild("advanced-search-criteria"));
+        _columns = _configureColumns(searchConfig.getChild("columns").getChild("default", false));
+    }
+    
+    @Override
+    public List<String> getContentTypes()
+    {
+        return _cTypes;
+    }
+    
+    @Override
+    public String getSearchUrl()
+    {
+        return _searchUrl;
+    }
+    
+    @Override
+    public String getSearchUrlPlugin()
+    {
+        return _searchUrlPlugin;
+    }
+
+    @Override
+    public String getExportCSVUrl()
+    {
+        return _exportCSVUrl;
+    }
+    
+    @Override
+    public String getExportCSVUrlPlugin()
+    {
+        return _exportCSVUrlPlugin;
+    }
+
+    @Override
+    public String getExportXMLUrl()
+    {
+        return _exportXMLUrl;
+    }
+
+    @Override
+    public String getExportXMLUrlPlugin()
+    {
+        return _exportXMLUrlPlugin;
+    }
+    
+    @Override
+    public String getPrintUrl()
+    {
+        return _printUrl;
+    }
+    
+    @Override
+    public String getPrintUrlPlugin()
+    {
+        return _printUrlPlugin;
+    }
+
+    @Override
+    public Map<String, SearchCriteria> getCriteria()
+    {
+        return _searchCriteria;
+    }
+
+    @Override
+    public Map<String, SearchCriteria> getAdvancedCriteria()
+    {
+        return _advancedSearchCriteria;
+    }
+
+    @Override
+    public String createSqlQuery(String sqlQuery)
+    {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public List<SearchColumn> getResultColumns()
+    {
+        return _columns;
+    }
+    
+    /**
+     * Configure the content type ids
+     * @param configuration The content types configuration
+     * @return The set of content type ids
+     * @throws ConfigurationException If an error occurs
+     */
+    protected List<String> _configureContentTypes(Configuration configuration) throws ConfigurationException
+    {
+        List<String> cTypes = new ArrayList<String>();
+        for (Configuration cType : configuration.getChildren("content-type"))
+        {
+            cTypes.add(cType.getAttribute("id"));
+        }
+        return cTypes;
+    }
+    
+    private void _configureSearchUrl(Configuration configuration)
+    {
+        _searchUrlPlugin = configuration.getAttribute("plugin", "cms");
+        _searchUrl = configuration.getChild("search-url").getValue("search/list.xml");
+    }
+    
+    private void _configureExportCSVUrl(Configuration configuration)
+    {
+        _exportCSVUrlPlugin = configuration.getAttribute("plugin", "cms");
+        _exportCSVUrl = configuration.getChild("export-csv-url").getValue("search/export.csv");
+    }
+    
+    private void _configureExportXMLUrl(Configuration configuration)
+    {
+        _exportXMLUrlPlugin = configuration.getAttribute("plugin", "cms");
+        _exportXMLUrl = configuration.getChild("export-xml-url").getValue("search/export.xml");
+    }
+    
+    private void _configurePrintUrl(Configuration configuration)
+    {
+        _printUrlPlugin = configuration.getAttribute("plugin", "cms");
+        _printUrl = configuration.getChild("print-url").getValue("search/print.html");
+    }
+    
+    private List<SearchColumn> _configureColumns (Configuration configuration) throws ConfigurationException
+    {
+        ContentType cType = getCommonContentTypeAncestor();
+        
+        List<SearchColumn> columns = new ArrayList<SearchColumn>();
+        
+        for (Configuration conf : configuration.getChildren("column"))
+        {
+            SearchColumn column = null;
+            
+            String metadataPath = conf.getAttribute("metadata-ref", null);
+            if (StringUtils.isNotEmpty(metadataPath))
+            {
+                if (!isMetadataExist(cType, metadataPath))
+                {
+                    throw new ConfigurationException("The metadata of path '" + metadataPath + "' does not exist for concerned content type(s).");
+                }
+                
+                MetadataDefinition metadataDefinition = getMetadataDefinition(cType, metadataPath);
+                
+                column = new SearchColumn();
+                column.setId(metadataPath.replaceAll("/", "."));
+                column.setTitle(_configureI18nizableText(conf.getChild("label", false), metadataDefinition.getLabel()));
+                column.setType(metadataDefinition.getType());
+                column.setWidth(conf.getChild("width").getValueAsInteger(200));
+                column.setRenderer(conf.getChild("renderer").getValue(null));
+                column.setHidden(conf.getChild("hidden").getValueAsBoolean(false));
+                column.setEditable(conf.getChild("editable").getValueAsBoolean(true));
+                column.setSortable(conf.getChild("sortable").getValueAsBoolean(true));
+                
+                columns.add(column);
+            }
+            else if (conf.getAttribute("system-ref", null) != null)
+            {
+                String property = conf.getAttribute("system-ref", null);
+                
+                if (property.equals("*"))
+                {
+                    for (String propertyName : SYSTEM_PROPERTY_NAMES)
+                    {
+                        if (!propertyName.equals("fulltext"))
+                        {
+                            column = getSystemSearchColumn(propertyName);
+                            columns.add(column);
+                        }
+                    }
+                }
+                else
+                {
+                    if (!isValidSystemProperty(property))
+                    {
+                        throw new ConfigurationException("The property '" + property + "' is not a valid system property.");
+                    }
+                    
+                    column = getSystemSearchColumn(property);
+                    column.setWidth(conf.getChild("width").getValueAsInteger(100));
+                    column.setRenderer(conf.getChild("renderer").getValue(""));
+                    column.setHidden(conf.getChild("hidden").getValueAsBoolean(false));
+                    column.setSortable(conf.getChild("hidden").getValueAsBoolean(true));
+                    
+                    columns.add(column);
+                }
+            }
+        }
+        return columns;
+    }
+    
+    private Map<String, SearchCriteria> _configureCriteria(Configuration configuration) throws ConfigurationException
+    {
+        ContentType cType = getCommonContentTypeAncestor();
+        
+        Map<String, SearchCriteria> criteria = new LinkedHashMap<String, SearchCriteria>();
+        
+        for (Configuration conf : configuration.getChildren("criteria"))
+        {
+            String metadataPath = conf.getAttribute("metadata-ref", null);
+            if (StringUtils.isNotEmpty(metadataPath))
+            {
+                if (metadataPath.equals("*"))
+                {
+                    MetadataSet metadataSet = cType.getMetadataSetForView("main");
+                    for (AbstractMetadataSetElement subMetadataSetElement : metadataSet.getElements())
+                    {
+                        // Get only simple metadata (ignore composites and repeaters)
+                        if (subMetadataSetElement instanceof MetadataDefinitionReference)
+                        {
+                            String metadataName = ((MetadataDefinitionReference) subMetadataSetElement).getMetadataName();
+                            MetadataDefinition metadataDefinition = cType.getMetadataDefinition(metadataName);
+                            
+                            MetadataSearchCriteria metaSC = _configureMetadataSearchCriteria(conf, metadataDefinition, metadataName);
+                            criteria.put(metaSC.getId(), metaSC);
+                        }
+                    }
+                }
+                else
+                {
+                    metadataPath = metadataPath.replaceAll("\\.", "/");
+                    if (!isMetadataExist(cType, metadataPath))
+                    {
+                        throw new ConfigurationException("The metadata of path '" + metadataPath + "' does not exist for concerned content type(s).");
+                    }
+                    
+                    MetadataDefinition metadataDefinition = getMetadataDefinition(cType, metadataPath);
+                    
+                    MetadataSearchCriteria metaSC = _configureMetadataSearchCriteria (conf, metadataDefinition, metadataPath);
+                    criteria.put(metaSC.getId(), metaSC);
+                }
+            }
+            else if (conf.getAttribute("system-ref", null) != null)
+            {
+                String property = conf.getAttribute("system-ref", null);
+                
+                if (property.equals("*"))
+                {
+                    for (String propertyName : SYSTEM_PROPERTY_NAMES)
+                    {
+                        SystemSearchCriteria systemSC = _createSystemSearchCriteria(propertyName, conf);
+                        criteria.put(systemSC.getId(), systemSC);
+                    }
+                }
+                else
+                {
+                    if (!isValidSystemProperty(property))
+                    {
+                        throw new ConfigurationException("The property '" + property + "' is not a valid system property.");
+                    }
+                    
+                    SystemSearchCriteria systemSC = _createSystemSearchCriteria(property, conf);
+                    criteria.put(systemSC.getId(), systemSC);
+                }
+            }
+            
+            // TODO custom-ref
+        }
+        return criteria;
+    }
+    
+    private MetadataSearchCriteria _configureMetadataSearchCriteria (Configuration conf, MetadataDefinition metadataDefinition, String metadataPath) throws ConfigurationException
+    {
+        MetadataSearchCriteria metadataSC = new MetadataSearchCriteria();
+        
+        metadataSC.setMetadataPath(metadataPath);
+        metadataSC.setLabel(_configureI18nizableText(conf.getChild("label", false), metadataDefinition.getLabel()));
+        metadataSC.setDescription(_configureI18nizableText(conf.getChild("description", false), metadataDefinition.getDescription()));
+        metadataSC.setType(metadataDefinition.getType());
+        metadataSC.setValidator(metadataDefinition.getValidator());
+        metadataSC.setEnumerator(metadataDefinition.getEnumerator());
+        
+        metadataSC.setWidget(conf.getChild("widget").getValue(metadataDefinition.getWidget()));
+        metadataSC.setWidgetParameters(metadataDefinition.getWidgetParameters());
+        
+        Operator operator = _configureTestOperator(conf);
+        metadataSC.setOperator(operator);
+        metadataSC.setHidden(conf.getAttributeAsBoolean("hidden", false));
+        metadataSC.setInitClassName(conf.getChild("oninit").getValue(null));
+        metadataSC.setChangeClassName(conf.getChild("onchange").getValue(null));
+        metadataSC.setSubmitClassName(conf.getChild("onsubmit").getValue(null));
+        metadataSC.setValue(conf.getChild("value").getValue(null));
+        
+        metadataSC.setId(SEARCH_CRITERIA_METADATA_PREFIX + metadataPath.replaceAll("/", ".") + "-" + operator.name().toLowerCase());
+        
+        return metadataSC;
+    }
+    
+    private SystemSearchCriteria _createSystemSearchCriteria (String propertyName, Configuration conf) throws ConfigurationException
+    {
+        SystemSearchCriteria systemSC = getSystemSearchCriteria(propertyName);
+        
+        systemSC.setLabel(_configureI18nizableText(conf.getChild("label", false), systemSC.getLabel()));
+        systemSC.setDescription(_configureI18nizableText(conf.getChild("description", false), systemSC.getDescription()));
+        
+        Operator operator = _configureTestOperator(conf);
+        systemSC.setOperator(operator);
+        systemSC.setHidden(conf.getAttributeAsBoolean("hidden", false));
+        systemSC.setInitClassName(conf.getChild("oninit").getValue(null));
+        systemSC.setChangeClassName(conf.getChild("onchange").getValue(null));
+        systemSC.setSubmitClassName(conf.getChild("onsubmit").getValue(null));
+        systemSC.setValue(conf.getChild("value").getValue(null));
+        
+        systemSC.setId(SEARCH_CRITERIA_SYSTEM_PREFIX + propertyName + "-" + operator.name().toLowerCase());
+        
+        return systemSC;
+    }
+    
+    private I18nizableText _configureI18nizableText(Configuration config, I18nizableText defaultValue) throws ConfigurationException
+    {
+        if (config != null)
+        {
+            String text = config.getValue();
+            boolean i18nSupported = config.getAttributeAsBoolean("i18n", false);
+            if (i18nSupported)
+            {
+                String catalogue = config.getAttribute("catalogue", null);
+                return new I18nizableText(catalogue, text);
+            }
+            else
+            {
+                return new I18nizableText(text);
+            }
+        }
+        else
+        {
+            return defaultValue;
+        }
+    }
+    
+    private Operator _configureTestOperator(Configuration configuration) throws ConfigurationException
+    {
+        try
+        {
+            return Operator.valueOf(configuration.getChild("test-operator").getValue("eq").toUpperCase());
+        }
+        catch (IllegalArgumentException e)
+        {
+            throw new ConfigurationException("Invalid operator", configuration, e);
+        }
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/SearchCriteria.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/SearchCriteria.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/SearchCriteria.java	(revision 0)
@@ -0,0 +1,143 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+import org.ametys.cms.contenttype.MetadataType;
+import org.ametys.plugins.repository.query.expression.Expression.Operator;
+import org.ametys.runtime.util.parameter.Parameter;
+
+
+/**
+ * This class represents a search criteria of a {@link SearchModel}
+ *
+ */
+public class SearchCriteria extends Parameter<MetadataType>
+{
+    private Operator _operator;
+    private String _onInitClassName;
+    private String _onSubmitClassName;
+    private String _onChangeClassName;
+    private boolean _hidden;
+    private Object _value;
+    
+    /**
+     * Get the operator such as 'equals to', 'greater than', 'not equals', ...) 
+     * @return the operator
+     */
+    public Operator getOperator()
+    {
+        return _operator;
+    }
+    
+    /**
+     * Set the operator
+     * @param operator The operator to set
+     */
+    public void setOperator(Operator operator)
+    {
+        this._operator = operator;
+    }
+
+    /**
+     * Get the JS class name to execute on 'init' event
+     * @return the JS class name to execute on 'init' event
+     */
+    public String getInitClassName()
+    {
+        return _onInitClassName;
+    }
+
+    /**
+     * Set the JS class name to execute on 'init' event
+     * @param className the JS class name 
+     */
+    public void setInitClassName(String className)
+    {
+        this._onInitClassName = className;
+    }
+
+    /**
+     * Get the JS class name to execute on 'submit' event
+     * @return the JS class name to execute on 'submit' event
+     */
+    public String getSubmitClassName()
+    {
+        return _onSubmitClassName;
+    }
+
+    /**
+     * Set the JS class name to execute on 'submit' event
+     * @param className the JS class name 
+     */
+    public void setSubmitClassName(String className)
+    {
+        this._onSubmitClassName = className;
+    }
+
+    /**
+     * Get the JS class name to execute on 'change' event
+     * @return the JS class name to execute on 'change' event
+     */
+    public String getChangeClassName()
+    {
+        return _onChangeClassName;
+    }
+
+    /**
+     * Set the JS class name to execute on 'change' event
+     * @param className the JS class name 
+     */
+    public void setChangeClassName(String className)
+    {
+        this._onChangeClassName = className;
+    }
+    
+    /**
+     * Determines if the criteria is hidden
+     * @return <code>true</code> if the criteria is hidden
+     */
+    public boolean isHidden()
+    {
+        return _hidden;
+    }
+    
+    /**
+     * Set the hidden property of the criteria
+     * @param hidden true to hide the search criteria
+     */
+    public void setHidden (boolean hidden)
+    {
+        this._hidden = hidden;
+    }
+
+    /**
+     * Get the value of search criteria
+     * @return the value
+     */
+    public Object getValue()
+    {
+        return this._value;
+    }
+    
+    /**
+     * Set the value of search criteria (if it is a hidden field)
+     * @param value The value to set
+     */
+    public void setValue (Object value)
+    {
+        this._value = value;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/model.xml
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/model.xml	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/model.xml	(revision 0)
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<SearchModel>
+
+	<content-types>
+		<content-type id=""/>
+		<content-type id=""/>
+	</content-types>
+	
+	<search-url></search-url>
+	<export-csv-url></export-csv-url>
+	<export-xml-url></export-xml-url>
+	<print-url></print-url>
+	
+	<simple-search-criteria>
+	
+		<criteria metadata-ref="title">
+			<test-operator>wd</test-operator>
+		</criteria>
+		
+		<criteria metadata-ref="foo" hidden="true"><!-- Hidden criteria -->
+			<value>foo</value>
+		</criteria>
+		
+		<criteria metadata-ref="contact/email"/> <!-- Criteria on composite metadata -->
+		
+		<criteria metadata-ref="site/department"/> <!-- Criteria on a referenced content -->
+		
+		<criteria system-ref="author"/>
+		<criteria system-ref="workflow-step"/>
+		
+		<criteria system-ref="lastModified">
+			<label i18n="true">SEARCH_LASTMODIFIED_AFTER</label>
+			<test-operator>gt</test-operator>
+		</criteria>
+		
+		<criteria system-ref="lastModified">
+			<label i18n="true">SEARCH_LASTMODIFIED_BEFORE</label>
+			<test-operator>lt</test-operator>
+		</criteria>
+		
+		<criteria system-ref="contentLanguage" hidden="true">
+			<value>fr</value>
+			<onsubmit>MyClassJSStatic</onsubmit>
+		</criteria>
+		
+	</simple-search-criteria>
+
+	<advanced-search-criteria/>
+
+	<sql-search-criteria/>	
+	
+	<columns>
+		<!-- Si default n'existe pas, on renvoie nos propres default -->
+		<default>
+			<column metadata-ref="title"/>
+			<column system-ref="author"/>
+		</default>
+		
+		<exclude>
+			<column system-ref="with-comments"/>
+		</exclude>
+		
+		<column system-ref="lastModified">
+			<width>130</width> <!-- Surcharge de la largeur -->
+		</column>
+	
+		<column system-ref="contentLanguage">
+			<renderer>MyClassJSStatic</renderer> <!-- Surcharge du renderer -->
+		</column>
+	</columns>
+</SearchModel>
\ No newline at end of file
Index: main/plugin-cms/src/org/ametys/cms/search/actions/GetSearchModelAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/actions/GetSearchModelAction.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/actions/GetSearchModelAction.java	(revision 0)
@@ -0,0 +1,196 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search.actions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.ProcessingException;
+import org.apache.cocoon.acting.ServiceableAction;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Redirector;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.environment.SourceResolver;
+
+import org.ametys.cms.search.SearchColumn;
+import org.ametys.cms.search.SearchCriteria;
+import org.ametys.cms.search.SearchModel;
+import org.ametys.cms.search.SearchModelExtensionPoint;
+import org.ametys.runtime.cocoon.JSonReader;
+import org.ametys.runtime.util.I18nizableText;
+import org.ametys.runtime.util.parameter.Enumerator;
+import org.ametys.runtime.util.parameter.ParameterHelper;
+import org.ametys.runtime.util.parameter.Validator;
+
+/**
+ * Get the search model configuration as JSON object
+ *
+ */
+public class GetSearchModelAction extends ServiceableAction
+{
+    private SearchModelExtensionPoint _searchModelManager;
+    
+    @Override
+    public void service(ServiceManager smanager) throws ServiceException
+    {
+        super.service(smanager);
+        _searchModelManager = (SearchModelExtensionPoint) smanager.lookup(SearchModelExtensionPoint.ROLE);
+    }
+    
+    @Override
+    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
+    {
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        String modelId = request.getParameter("model");
+        
+        SearchModel model = _searchModelManager.getExtension(modelId);
+        
+        Map<String, Object> jsonObject = _searchModel2JsonObject (model);
+        
+        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
+        return EMPTY_MAP;
+    }
+    
+    private Map<String, Object> _searchModel2JsonObject (SearchModel model) throws ProcessingException
+    {
+        Map<String, Object> jsonObject = new HashMap<String, Object>();
+        jsonObject.put("searchUrl", model.getSearchUrl());
+        jsonObject.put("searchUrlPlugin", model.getSearchUrlPlugin());
+        jsonObject.put("exportCVSUrl", model.getExportCSVUrl());
+        jsonObject.put("exportCVSUrlPlugin", model.getExportCSVUrlPlugin());
+        jsonObject.put("exportXMLUrl", model.getExportXMLUrlPlugin());
+        jsonObject.put("exportXMLUrlPlugin", model.getExportXMLUrl());
+        jsonObject.put("printUrl", model.getPrintUrl());
+        jsonObject.put("printUrlPlugin", model.getPrintUrlPlugin());
+        
+        jsonObject.put("simple-criteria", _criteria2JsonObject (model.getCriteria()));
+        jsonObject.put("columns", _column2JsonObject (model.getResultColumns()));
+        
+        return jsonObject;
+    }
+    
+    private Map<String, Object> _column2JsonObject (List<SearchColumn> columns)
+    {
+        Map<String, Object> jsonObject = new LinkedHashMap<String, Object>();
+        
+        for (SearchColumn column : columns)
+        {
+            jsonObject.put(column.getId(), _column2JsonObject(column));
+        }
+        
+        return jsonObject;
+    }
+    
+    private Map<String, Object> _column2JsonObject (SearchColumn column)
+    {
+        Map<String, Object> jsonObject = new LinkedHashMap<String, Object>();
+        
+        jsonObject.put("title", column.getTitle());
+        jsonObject.put("type", column.getType().name());
+        jsonObject.put("hidden", column.isHidden());
+        jsonObject.put("renderer", column.getRenderer());
+        jsonObject.put("width", column.getWidth());
+        jsonObject.put("mapping", column.getMapping());
+        
+        return jsonObject;
+    }
+    
+    private Map<String, Object> _criteria2JsonObject (Map<String, SearchCriteria> criteria) throws ProcessingException
+    {
+        Map<String, Object> jsonObject = new LinkedHashMap<String, Object>();
+        
+        for (SearchCriteria sc : criteria.values())
+        {
+            jsonObject.put(sc.getId(), _criteria2JsonObject(sc));
+        }
+        
+        return jsonObject;
+    }
+    
+    private Map<String, Object> _criteria2JsonObject (SearchCriteria criteria) throws ProcessingException
+    {
+        Map<String, Object> jsonObject = new LinkedHashMap<String, Object>();
+        
+        jsonObject.put("label", criteria.getLabel());
+        jsonObject.put("description", criteria.getDescription());
+        jsonObject.put("type", criteria.getType().name());
+        
+        Validator validator = criteria.getValidator();
+        if (validator != null)
+        {
+            Map<String, Object> val2Json = validator.toJson();
+            if (val2Json.containsKey("mandatory"))
+            {
+                // Override mandatory property
+                val2Json.put("mandatory", false);
+            }
+            jsonObject.put("validation", val2Json);
+        }
+        
+        String widget = criteria.getWidget();
+        
+        if (widget != null)
+        {
+            jsonObject.put("widget", widget);
+        }
+        
+        Map<String, I18nizableText> widgetParameters = criteria.getWidgetParameters();
+        if (widgetParameters != null && widgetParameters.size() > 0)
+        {
+            jsonObject.put("widget-params", criteria.getWidgetParameters());
+        }
+        
+        Object defaultValue = criteria.getDefaultValue();
+        if (defaultValue != null)
+        {
+            jsonObject.put("default-value", criteria.getDefaultValue());
+        }
+
+        Enumerator enumerator = criteria.getEnumerator();
+        if (enumerator != null)
+        {
+            try
+            {
+                List<Map<String, Object>> options = new ArrayList<Map<String, Object>>();
+                
+                for (Map.Entry<Object, I18nizableText> entry : enumerator.getEntries().entrySet())
+                {
+                    String valueAsString = ParameterHelper.valueToString(entry.getKey());
+                    I18nizableText entryLabel = entry.getValue();
+                    
+                    Map<String, Object> option = new HashMap<String, Object>();
+                    option.put("label", entryLabel != null ? entryLabel : valueAsString);
+                    option.put("value", valueAsString);
+                    options.add(option);
+                }
+                
+                jsonObject.put("enumeration", options);
+            }
+            catch (Exception e)
+            {
+                throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e);
+            }
+        }
+        
+        return jsonObject;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/SearchColumn.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/SearchColumn.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/SearchColumn.java	(revision 0)
@@ -0,0 +1,199 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+import org.ametys.cms.contenttype.MetadataType;
+import org.ametys.runtime.util.I18nizableText;
+
+
+/**
+ * This class represents a result column
+ *
+ */
+public class SearchColumn
+{
+    private String _id;
+    private I18nizableText _title;
+    private int _width;
+    private String _renderer;
+    private boolean _hidden;
+    private MetadataType _type;
+    private String _mapping;
+    private boolean _editable;
+    private boolean _sortable;
+    
+    /**
+     * Get the id 
+     * @return The id 
+     */
+    public String getId()
+    {
+        return this._id;
+    }
+    
+    /**
+     * Set the id
+     * @param id The id to set
+     */
+    public void setId(String id)
+    {
+        this._id = id;
+    }
+    
+    /**
+     * Get the path expression to extract the field value
+     * @return The path expression 
+     */
+    public String getMapping()
+    {
+        return this._mapping != null ? this._mapping : "metadata/" + this._id.replaceAll("\\.", "/");
+    }
+    
+    /**
+     * Set the path expression to extract the field value
+     * @param mapping The the path expression
+     */
+    public void setMapping(String mapping)
+    {
+        this._mapping = mapping;
+    }
+    
+    /**
+     * Get the column title
+     * @return the column title
+     */
+    public I18nizableText getTitle()
+    {
+        return _title;
+    }
+    
+    /**
+     * Set the column title
+     * @param title The title to set
+     */
+    public void setTitle(I18nizableText title)
+    {
+        this._title = title;
+    }
+    
+    /**
+     * The column's width
+     * @return The width
+     */
+    public int getWidth()
+    {
+        return _width;
+    }
+    
+    /**
+     * Set the column's width
+     * @param width The width to set
+     */
+    public void setWidth (int width)
+    {
+        this._width = width;
+    }
+    
+    /**
+     * Determines if the column is hidden by default
+     * @return <code>true</code> if the column is hidden by default  
+     */
+    public boolean isHidden ()
+    {
+        return this._hidden;
+    }
+    
+    /**
+     * Set the hidden property
+     * @param hidden <code>true</code> to hidden the columns by default
+     */
+    public void setHidden (boolean hidden)
+    {
+        this._hidden = hidden;
+    }
+    
+    /**
+     * Determines if the property is editable
+     * @return <code>true</code> if the property is editable
+     */
+    public boolean isEditable ()
+    {
+        return this._editable;
+    }
+    
+    /**
+     * Set the sortable property
+     * @param sortable <code>true</code> to authorized sort
+     */
+    public void setSortable (boolean sortable)
+    {
+        this._sortable = sortable;
+    }
+    
+    /**
+     * Determines if the column is sortable
+     * @return <code>true</code> if the column is sortable
+     */
+    public boolean isSortable ()
+    {
+        return this._sortable;
+    }
+    
+    /**
+     * Set the editable property
+     * @param editable <code>true</code> to authorized edition
+     */
+    public void setEditable (boolean editable)
+    {
+        this._editable = editable;
+    }
+    
+    /**
+     * Get the JS class name for renderer
+     * @return The renderer
+     */
+    public String getRenderer()
+    {
+        return _renderer;
+    }
+    
+    /**
+     * Set the JS class name for renderer
+     * @param renderer The renderer
+     */
+    public void setRenderer(String renderer)
+    {
+        this._renderer = renderer;
+    }
+    
+    /**
+     * Get the type
+     * @return the type
+     */
+    public MetadataType getType ()
+    {
+        return this._type;
+    }
+    
+    /**
+     * Set the type
+     * @param type The type
+     */
+    public void setType (MetadataType type)
+    {
+        this._type = type;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/SystemSearchCriteria.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/SystemSearchCriteria.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/SystemSearchCriteria.java	(revision 0)
@@ -0,0 +1,43 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+/**
+ * This class is a search criteria on a system prorety (author, lastModified, with-comments, ...)
+ *
+ */
+public class SystemSearchCriteria extends SearchCriteria
+{
+    private String _property;
+    
+    /**
+     * Get the system property name
+     * @return The property name
+     */
+    public String getPropertyName()
+    {
+        return _property;
+    }
+    
+    /**
+     * Set the system property name
+     * @param property The property name
+     */
+    public void setPropertyName(String property)
+    {
+        _property = property;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/generators/SearchGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/generators/SearchGenerator.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/generators/SearchGenerator.java	(revision 0)
@@ -0,0 +1,395 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search.generators;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.ProcessingException;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.generation.ServiceableGenerator;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.lang.StringUtils;
+import org.xml.sax.SAXException;
+
+import org.ametys.cms.contenttype.AbstractMetadataSetElement;
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.MetadataDefinitionReference;
+import org.ametys.cms.contenttype.MetadataManager;
+import org.ametys.cms.contenttype.MetadataSet;
+import org.ametys.cms.languages.Language;
+import org.ametys.cms.languages.LanguagesManager;
+import org.ametys.cms.repository.Content;
+import org.ametys.cms.repository.RequestAttributeWorkspaceSelector;
+import org.ametys.cms.repository.WorkflowAwareContent;
+import org.ametys.cms.repository.comment.CommentableContent;
+import org.ametys.cms.search.SearchColumn;
+import org.ametys.cms.search.SearchModel;
+import org.ametys.cms.search.SearchModelExtensionPoint;
+import org.ametys.plugins.repository.AmetysObjectIterable;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.workflow.Workflow;
+import org.ametys.runtime.user.User;
+import org.ametys.runtime.user.UsersManager;
+import org.ametys.runtime.util.I18nizableText;
+import org.ametys.runtime.util.parameter.ParameterHelper;
+
+import com.opensymphony.workflow.loader.StepDescriptor;
+import com.opensymphony.workflow.loader.WorkflowDescriptor;
+import com.opensymphony.workflow.spi.Step;
+
+/**
+ * Search contents
+ *
+ */
+public class SearchGenerator extends ServiceableGenerator
+{
+    /** The namespace uri */
+    protected static final String __I18N_NAMESPACE_URI = "http://apache.org/cocoon/i18n/2.1";
+    
+    /** The search model manager */
+    protected SearchModelExtensionPoint _searchModelManager;
+    /** The {@link AmetysObjectResolver} */
+    protected AmetysObjectResolver _resolver;
+    /** The ContentType Manager*/
+    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
+    /** The language manager */
+    protected LanguagesManager _languagesManager;
+    /** The metadata manager */
+    protected MetadataManager _metadataManager;
+    /** The workflow */
+    protected Workflow _workflow;
+    /** The user manager */
+    protected UsersManager _usersManager;
+    
+    /** The cache for users' name */
+    protected Map<String, String> _userCache;
+    
+    
+    @Override
+    public void service(ServiceManager smanager) throws ServiceException
+    {
+        super.service(smanager);
+        _searchModelManager = (SearchModelExtensionPoint) smanager.lookup(SearchModelExtensionPoint.ROLE);
+        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
+        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+        _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
+        _metadataManager = (MetadataManager) manager.lookup(MetadataManager.ROLE);
+        _workflow = (Workflow) manager.lookup(Workflow.ROLE);
+        _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE);
+    }
+    
+    @Override
+    public void generate() throws IOException, SAXException, ProcessingException
+    {
+        // Clear cache
+        clearUserCache();
+        
+        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
+        
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        String modelId = (String) jsParameters.get("model");
+        
+        SearchModel model = _searchModelManager.getExtension(modelId);
+        
+        String workspaceName = parameters.getParameter("repository-workspace", null);
+        
+        // Get parameters from request
+        int begin = jsParameters.get("start") != null ? (Integer) jsParameters.get("start") : 0; // Index of search
+        int offset = jsParameters.get("limit") != null ? (Integer) jsParameters.get("limit") : Integer.MAX_VALUE; // Number of results to SAX
+        
+        String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
+        
+        try
+        {
+            if (StringUtils.isNotEmpty(workspaceName))
+            {
+                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
+            }
+            
+            String xPathQuery = model.createQuery((Map<String, Object>) jsParameters.get("values"));
+            
+            // SAX results between begin and begin + offset
+            contentHandler.startDocument();
+            contentHandler.startPrefixMapping("i18n", __I18N_NAMESPACE_URI);
+    
+            XMLUtils.startElement(contentHandler, "contents");
+            
+            AmetysObjectIterable<Content> it = _resolver.query(xPathQuery);
+            // Index of search
+            it.skip(begin);
+            
+            int index = 0;
+            while (it.hasNext() && index < offset)
+            {
+                Content content = it.next();
+                
+                // TODO Check right
+                saxContent(content, model);
+                index++;
+            }
+            it.close();
+    
+            XMLUtils.createElement(contentHandler, "total", String.valueOf(it.getSize()));
+            XMLUtils.endElement(contentHandler, "contents");
+    
+            contentHandler.endPrefixMapping("i18n");
+            contentHandler.endDocument();
+        }
+        catch (Exception e)
+        {
+            getLogger().error("Cannot search for contents : " + e.getMessage(), e);
+            throw new ProcessingException("Cannot search for contents : " + e.getMessage(), e);
+        }
+        finally
+        {
+            if (StringUtils.isNotEmpty(workspaceName))
+            {
+                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
+            }
+        }
+    }
+    
+    /**
+     * SAX a content
+     * @param content The content to SAX
+     * @param model The search model
+     * @throws SAXException if errors occurs while SAXing
+     * @throws RepositoryException if errors occurs while retrieving content's metadata
+     * @throws IOException on error if errors occurs while SAXing
+     */
+    protected void saxContent (Content content, SearchModel model) throws SAXException, RepositoryException, IOException
+    {
+        List<SearchColumn> columns = model.getResultColumns();
+        
+        AttributesImpl attrs = new AttributesImpl();
+        
+        ContentType contentType =  _contentTypeExtensionPoint.getExtension(content.getType());
+        
+        attrs.addCDATAAttribute("smallIcon", contentType != null ? contentType.getSmallIcon() : "/plugins/cms/resources/img/contenttype/unknown-small.png");
+        attrs.addCDATAAttribute("mediumIcon", contentType != null ? contentType.getMediumIcon() : "/plugins/cms/resources/img/contenttype/unknown-medium.png");
+        attrs.addCDATAAttribute("largeIcon", contentType != null ? contentType.getLargeIcon() : "/plugins/cms/resources/img/contenttype/unknown-large.png");
+        attrs.addCDATAAttribute("id", content.getId());
+        attrs.addCDATAAttribute("name", content.getName());
+        attrs.addCDATAAttribute("title", content.getTitle());
+        attrs.addCDATAAttribute("language", content.getLanguage());
+        
+        XMLUtils.startElement(contentHandler, "content", attrs);
+
+        // System properties
+        saxSystemProperties(columns, content, contentType);
+        
+        // Metadata
+        XMLUtils.startElement(contentHandler, "metadata");
+        MetadataSet metadataSet = getMetadataToSAX(model, model.getResultColumns(), contentType);
+        _metadataManager.saxReadableMetadata(contentHandler, content, metadataSet);
+        XMLUtils.endElement(contentHandler, "metadata");
+        
+        XMLUtils.endElement(contentHandler, "content");
+    }
+    
+    /**
+     * SAX the system properties
+     * @param columns The
+     * @param content
+     * @param cType
+     * @throws SAXException
+     * @throws RepositoryException
+     */
+    protected void saxSystemProperties (List<SearchColumn> columns, Content content, ContentType cType) throws SAXException, RepositoryException
+    {
+        for (SearchColumn searchColumn : columns)
+        {
+            String id = searchColumn.getId();
+            
+            if ("workflowStep".equals(id) && content instanceof WorkflowAwareContent)
+            {
+                saxContentCurrentState((WorkflowAwareContent) content);
+            }
+            else if ("contentType".equals(id) && cType != null)
+            {
+                cType.getLabel().toSAX(contentHandler, "content-type");
+            }
+            else if ("creator".equals(id))
+            {
+                String login = content.getCreator();
+                XMLUtils.createElement(contentHandler, "creator", login);
+                XMLUtils.createElement(contentHandler, "creatorDisplay", getUserFullName(login));
+            }
+            else if ("contributor".equals(id))
+            {
+                String login = content.getLastContributor();
+                XMLUtils.createElement(contentHandler, "contributor", login);
+                XMLUtils.createElement(contentHandler, "contributorDisplay", getUserFullName(login));
+            }
+            else if ("lastModified".equals(id))
+            {
+                XMLUtils.createElement(contentHandler, "lastModified", ParameterHelper.valueToString(content.getLastModified()));
+            }
+            else if ("comments".equals(id))
+            {
+                if (content instanceof CommentableContent)
+                {
+                    boolean hasComment = ((CommentableContent) content).getComments(true, true).size() != 0;
+                    XMLUtils.createElement(contentHandler, "comments", String.valueOf(hasComment));
+                }
+            }
+            else if ("contentLanguage".equals(id))
+            {
+                Language language = _languagesManager.getAvailableLanguages().get(content.getLanguage());
+                if (language != null)
+                {
+                    XMLUtils.createElement(contentHandler, "contentLanguageIcon", language.getSmallIcon());
+                }
+                
+                XMLUtils.createElement(contentHandler, "contentLanguage", content.getLanguage());
+            }
+        }
+    }
+    /**
+     * Extracts the metadata to SAX
+     * @param model The search model
+     * @param columns The result columns
+     * @param contentType The content type
+     * @return the metadata to SAX in a {@link MetadataSet}
+     */
+    protected MetadataSet getMetadataToSAX (SearchModel model, List<SearchColumn> columns, ContentType contentType)
+    {
+        MetadataSet metadataSet = new MetadataSet();
+        
+        AbstractMetadataSetElement metadataSetElmt = metadataSet;
+        for (SearchColumn column : columns)
+        {
+            String id = column.getId();
+            
+            if (!model.isValidSystemProperty(id))
+            {
+                String[] path = id.split("\\/|\\.");
+                
+                if (contentType.getMetadataDefinition(path[0]) != null)
+                {
+                    for (String metadataName : path)
+                    {
+                        MetadataDefinitionReference metadataRef = metadataSetElmt.getMetadataDefinitionReference(metadataName);
+                        if (metadataRef == null)
+                        {
+                            metadataRef = new MetadataDefinitionReference(metadataName);
+                        }
+                        metadataSetElmt.addElement(metadataRef);
+                        metadataSetElmt = metadataRef;
+                    }
+                    
+                    metadataSetElmt = metadataSet;
+                }
+            }
+        }
+        
+        return metadataSet;
+    }
+    
+    /**
+     * SAX workflow current step of a {@link Content}
+     * @param content The content
+     * @throws SAXException if an error occurs while SAXing
+     * @throws RepositoryException if an error occurs while retrieving current step
+     */
+    protected void saxContentCurrentState(WorkflowAwareContent content) throws SAXException, RepositoryException
+    {
+        int currentStepId = 0;
+
+        long workflowId = content.getWorkflowId();
+        Iterator iterator = _workflow.getCurrentSteps(workflowId).iterator();
+        
+        while (iterator.hasNext())
+        {
+            Step step = (Step) iterator.next();
+            currentStepId = step.getStepId();
+        }
+        
+        
+        String workflowName = _workflow.getWorkflowName(workflowId);
+        WorkflowDescriptor workflowDescriptor = _workflow.getWorkflowDescriptor(workflowName);
+        StepDescriptor stepDescriptor = workflowDescriptor.getStep(currentStepId);
+        
+        if (stepDescriptor != null)
+        {
+            I18nizableText workflowStepName = new I18nizableText("application", stepDescriptor.getName());
+            
+            AttributesImpl attr = new AttributesImpl();
+            attr.addAttribute("", "id", "id", "CDATA", String.valueOf(currentStepId));
+            
+            XMLUtils.startElement(contentHandler, "workflow-step", attr);
+            workflowStepName.toSAX(contentHandler);
+            XMLUtils.endElement(contentHandler, "workflow-step");
+            
+            String[] icons = new String[]{"-small", "-medium", "-large"};
+            for (String icon : icons)
+            {
+                if ("application".equals(workflowStepName.getCatalogue()))
+                {
+                    XMLUtils.createElement(contentHandler, "workflow-icon" + icon, "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + icon + ".png");
+                }
+                else
+                {
+                    String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
+                    XMLUtils.createElement(contentHandler, "workflow-icon" + icon, "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + icon + ".png");
+                }
+            }
+        }
+    }
+    
+    /**
+     * Clear the user cache
+     */
+    protected void clearUserCache ()
+    {
+        _userCache = new HashMap<String, String>();
+    }
+    
+    /**
+     * Get the user full name
+     * @param login the user login
+     * @return the user name
+     */
+    protected String getUserFullName (String login)
+    {
+        String name = _userCache.get(login);
+        if (name != null)
+        {
+            return name;
+        }
+
+        User user = _usersManager.getUser(login);
+        if (user == null)
+        {
+            return "";
+        }
+
+        name = user.getFullName();
+        _userCache.put(login, name);
+        return name;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/search/MetadataSearchCriteria.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/MetadataSearchCriteria.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/search/MetadataSearchCriteria.java	(revision 0)
@@ -0,0 +1,43 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed 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.ametys.cms.search;
+
+/**
+ * This class is a search criteria on a metadata of a content
+ *
+ */
+public class MetadataSearchCriteria extends SearchCriteria
+{
+    String _metadataPath;
+    
+    /**
+     * Get the path of metadata (separated by '/')
+     * @return the path of metadata
+     */
+    public String getMetadataPath()
+    {
+        return _metadataPath;
+    }
+    
+    /**
+     * Set the metadata path 
+     * @param metadataPath The metadata path separated by '/'.
+     */
+    public void setMetadataPath (String metadataPath)
+    {
+        _metadataPath = metadataPath;
+    }
+}