com.manydesigns.portofino.pageactions.crud.AbstractCrudAction.java Source code

Java tutorial

Introduction

Here is the source code for com.manydesigns.portofino.pageactions.crud.AbstractCrudAction.java

Source

/*
 * Copyright (C) 2005-2014 ManyDesigns srl.  All rights reserved.
 * http://www.manydesigns.com/
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package com.manydesigns.portofino.pageactions.crud;

import com.manydesigns.elements.ElementsThreadLocals;
import com.manydesigns.elements.FormElement;
import com.manydesigns.elements.Mode;
import com.manydesigns.elements.annotations.*;
import com.manydesigns.elements.blobs.Blob;
import com.manydesigns.elements.blobs.BlobManager;
import com.manydesigns.elements.blobs.BlobUtils;
import com.manydesigns.elements.fields.FileBlobField;
import com.manydesigns.elements.fields.Field;
import com.manydesigns.elements.fields.SelectField;
import com.manydesigns.elements.fields.TextField;
import com.manydesigns.elements.forms.FieldSet;
import com.manydesigns.elements.forms.*;
import com.manydesigns.elements.messages.SessionMessages;
import com.manydesigns.elements.options.DefaultSelectionProvider;
import com.manydesigns.elements.options.DisplayMode;
import com.manydesigns.elements.options.SearchDisplayMode;
import com.manydesigns.elements.options.SelectionProvider;
import com.manydesigns.elements.reflection.ClassAccessor;
import com.manydesigns.elements.reflection.PropertyAccessor;
import com.manydesigns.elements.servlet.MutableHttpServletRequest;
import com.manydesigns.elements.text.OgnlTextFormat;
import com.manydesigns.elements.util.MimeTypes;
import com.manydesigns.elements.util.Util;
import com.manydesigns.elements.xml.XhtmlBuffer;
import com.manydesigns.portofino.PortofinoProperties;
import com.manydesigns.portofino.buttons.GuardType;
import com.manydesigns.portofino.buttons.annotations.Button;
import com.manydesigns.portofino.buttons.annotations.Buttons;
import com.manydesigns.portofino.buttons.annotations.Guard;
import com.manydesigns.portofino.di.Inject;
import com.manydesigns.portofino.dispatcher.PageInstance;
import com.manydesigns.portofino.modules.BaseModule;
import com.manydesigns.portofino.pageactions.AbstractPageAction;
import com.manydesigns.portofino.pageactions.PageActionLogic;
import com.manydesigns.portofino.pageactions.crud.configuration.CrudConfiguration;
import com.manydesigns.portofino.pageactions.crud.configuration.CrudProperty;
import com.manydesigns.portofino.pageactions.crud.reflection.CrudAccessor;
import com.manydesigns.portofino.security.AccessLevel;
import com.manydesigns.portofino.security.RequiresPermissions;
import com.manydesigns.portofino.util.PkHelper;
import com.manydesigns.portofino.util.ShortNameUtils;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.util.UrlBuilder;
import ognl.OgnlContext;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.json.JSONException;
import org.json.JSONStringer;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>A generic PageAction offering CRUD functionality, independently on the underlying data source.</p>
 * <p>Out of the box, instances of this class are capable of the following:
 *   <ul>
 *      <li>Presenting search, create, read, delete, update operations (the last two also in bulk mode) to the user,
 *          while delegating the actual implementation (e.g. accessing a database table, calling a web service,
 *          querying a JSON data source, etc.) to concrete subclasses;
 *      </li>
 *      <li>Performing exports to various formats (Pdf, Excel) of the Read view and the Search view,
 *          with the possibility for subclasses to customize the exports;</li>
 *      <li>Managing selection providers to constrain certain properties to values taken from a list, and aid
 *          the user in inserting those values (e.g. picking colours from a combo box, or cities with an
 *          autocompleted input field); the actual handling of selection providers is delegated to a
 *          companion object of type {@link SelectionProviderSupport} which must be provided by the concrete
 *          subclasses;</li>
 *      <li>Handling permissions so that only enabled users may create, edit or delete objects;</li>
 *      <li>Offering hooks for subclasses to easily customize certain key functions (e.g. execute custom code
 *          before or after saving an object).</li>
 *   </ul>
 * </p>
 * <p>This PageAction can handle a varying number of URL path parameters. Each parameter is assumed to be part
 * of an object identifier - for example, a database primary key (single or multi-valued). When no parameter is
 * specified, the page is in search mode. When the correct number of parameters is provided, the action attempts
 * to load an object with the appropriate identifier (for example, by loading a row from a database table with
 * the corresponding primary key). As any other page, crud pages can have children, and they always prevail over
 * the object key: a crud page with a child named &quot;child&quot; will never attempt to load an object with key
 * &quot;child&quot;.</p>
 * <!-- TODO popup mode -->
 *
 * @param <T> the types of objects that this crud can handle.
 *
 * @author Paolo Predonzani     - paolo.predonzani@manydesigns.com
 * @author Angelo Lupo          - angelo.lupo@manydesigns.com
 * @author Giampiero Granatella - giampiero.granatella@manydesigns.com
 * @author Alessio Stalla       - alessio.stalla@manydesigns.com
 */
public abstract class AbstractCrudAction<T> extends AbstractPageAction {
    public static final String copyright = "Copyright (c) 2005-2014, ManyDesigns srl";

    public final static String SEARCH_STRING_PARAM = "searchString";
    public final static String prefix = "";
    public final static String searchPrefix = prefix + "search_";

    //**************************************************************************
    // Permissions
    //**************************************************************************

    /**
     * Constants for the permissions supported by instances of this class. Subclasses are recommended to
     * support at least the permissions defined here.
     */
    public static final String PERMISSION_CREATE = "crud-create", PERMISSION_EDIT = "crud-edit",
            PERMISSION_DELETE = "crud-delete";

    public static final Logger logger = LoggerFactory.getLogger(AbstractCrudAction.class);

    //--------------------------------------------------------------------------
    // Web parameters
    //--------------------------------------------------------------------------

    public String[] pk;
    public String propertyName;
    public String[] selection;
    public String searchString;
    public String successReturnUrl;
    public Integer firstResult;
    public Integer maxResults;
    public String sortProperty;
    public String sortDirection;
    public boolean searchVisible;

    //--------------------------------------------------------------------------
    // Popup
    //--------------------------------------------------------------------------

    protected String popupCloseCallback;

    //--------------------------------------------------------------------------
    // UI forms
    //--------------------------------------------------------------------------

    public SearchForm searchForm;
    public TableForm tableForm;
    public Form form;

    //--------------------------------------------------------------------------
    // Selection providers
    //--------------------------------------------------------------------------

    protected SelectionProviderSupport selectionProviderSupport;

    protected String relName;
    protected int selectionProviderIndex;
    protected String selectFieldMode;
    protected String labelSearch;

    //--------------------------------------------------------------------------
    // Data objects
    //--------------------------------------------------------------------------

    public ClassAccessor classAccessor;
    public PkHelper pkHelper;

    public T object;
    public List<? extends T> objects;

    @Inject(BaseModule.DEFAULT_BLOB_MANAGER)
    protected BlobManager blobManager;

    @Inject(BaseModule.TEMPORARY_BLOB_MANAGER)
    protected BlobManager temporaryBlobManager;

    //--------------------------------------------------------------------------
    // Configuration
    //--------------------------------------------------------------------------

    public CrudConfiguration crudConfiguration;
    public Form crudConfigurationForm;
    public TableForm propertiesTableForm;
    public CrudPropertyEdit[] propertyEdits;
    public TableForm selectionProvidersForm;
    public CrudSelectionProviderEdit[] selectionProviderEdits;

    //--------------------------------------------------------------------------
    // Navigation
    //--------------------------------------------------------------------------

    protected ResultSetNavigation resultSetNavigation;

    //--------------------------------------------------------------------------
    // Crud operations
    //--------------------------------------------------------------------------

    /**
     * Loads a list of objects filtered using the current search criteria and limited by the current
     * first and max results parameters. If the load is successful, the implementation must assign
     * the result to the <code>objects</code> field.
     */
    public abstract void loadObjects();

    /**
     * Loads an object by its identifier and returns it. The object must satisfy the current search criteria.
     * @param pkObject the object used as an identifier; the actual implementation is regulated by subclasses.
     * The only constraint is that it is serializable.
     * @return the loaded object, or null if it couldn't be found or it didn't satisfy the search criteria.
     */
    protected abstract T loadObjectByPrimaryKey(Serializable pkObject);

    /**
     * Saves a new object to the persistent storage. The actual implementation is left to subclasses.
     * @param object the object to save.
     * @throws RuntimeException if the object could not be saved.
     */
    protected abstract void doSave(T object);

    /**
     * Saves an existing object to the persistent storage. The actual implementation is left to subclasses.
     * @param object the object to update.
     * @throws RuntimeException if the object could not be saved.
     */
    protected abstract void doUpdate(T object);

    /**
     * Deletes an object from the persistent storage. The actual implementation is left to subclasses.
     * @param object the object to delete.
     * @throws RuntimeException if the object could not be deleted.
     */
    protected abstract void doDelete(T object);

    @DefaultHandler
    public Resolution execute() {
        if (object == null) {
            return doSearch();
        } else {
            return read();
        }
    }

    /**
     * @see #loadObjectByPrimaryKey(java.io.Serializable)
     * @param identifier the object identifier in String form
     */
    protected void loadObject(String... identifier) {
        Serializable pkObject = pkHelper.getPrimaryKey(identifier);
        object = loadObjectByPrimaryKey(pkObject);
    }

    //**************************************************************************
    // Search
    //**************************************************************************

    @Buttons({ @Button(list = "crud-search-form", key = "search", order = 1, type = Button.TYPE_PRIMARY),
            @Button(list = "crud-search-form-default-button", key = "search") })
    public Resolution search() {
        searchVisible = true;
        searchString = null;
        sortProperty = null;
        sortDirection = null;
        firstResult = null;
        maxResults = null;
        return doSearch();
    }

    protected Resolution doSearch() {
        if (!isConfigured()) {
            logger.debug("Crud not correctly configured");
            return forwardToPageActionNotConfigured();
        }

        try {
            executeSearch();
            if (PageActionLogic.isEmbedded(this)) {
                return getEmbeddedSearchView();
            } else {
                returnUrl = new UrlBuilder(context.getLocale(), Util.getAbsoluteUrl(context.getActionPath()), false)
                        .toString();
                returnUrl = appendSearchStringParamIfNecessary(returnUrl);
                return getSearchView();
            }
        } catch (Exception e) {
            logger.warn("Crud not correctly configured", e);
            return forwardToPageActionNotConfigured();
        }
    }

    public Resolution getSearchResultsPage() {
        if (!isConfigured()) {
            logger.debug("Crud not correctly configured");
            return new ErrorResolution(500, "Crud not correctly configured");
        }

        try {
            executeSearch();
            context.getRequest().setAttribute("actionBean", this);
            return getSearchResultsPageView();
        } catch (Exception e) {
            logger.warn("Crud not correctly configured", e);
            return new ErrorResolution(500, "Crud not correctly configured");
        }
    }

    protected void executeSearch() {
        setupSearchForm();
        if (maxResults == null) {
            //Load only the first page if the crud is paginated
            maxResults = getCrudConfiguration().getRowsPerPage();
        }
        loadObjects();
        setupTableForm(Mode.VIEW);
        BlobUtils.loadBlobs(tableForm, getBlobManager(), false);
    }

    public Resolution jsonSearchData() throws JSONException {
        setupSearchForm();
        loadObjects();

        long totalRecords = getTotalSearchRecords();

        setupTableForm(Mode.VIEW);
        BlobUtils.loadBlobs(tableForm, getBlobManager(), false);
        JSONStringer js = new JSONStringer();
        js.object().key("recordsReturned").value(objects.size()).key("totalRecords").value(totalRecords)
                .key("startIndex").value(firstResult == null ? 0 : firstResult).key("Result").array();
        for (TableForm.Row row : tableForm.getRows()) {
            js.object().key("__rowKey").value(row.getKey());
            fieldsToJson(js, row);
            js.endObject();
        }
        js.endArray();
        js.endObject();
        String jsonText = js.toString();
        return new StreamingResolution(MimeTypes.APPLICATION_JSON_UTF8, jsonText);
    }

    /**
     * Returns the number of objects matching the current search criteria, not considering set limits
     * (first and max results).
     * @return the number of objects.
     */
    public abstract long getTotalSearchRecords();

    @Button(list = "crud-search-form", key = "reset.search", order = 2)
    public Resolution resetSearch() {
        return new RedirectResolution(context.getActionPath()).addParameter("searchVisible", true);
    }

    //**************************************************************************
    // Read
    //**************************************************************************

    public Resolution read() {
        if (!crudConfiguration.isLargeResultSet()) {
            setupSearchForm(); // serve per la navigazione del result set
            loadObjects();
            setupPagination();
        }

        setupForm(Mode.VIEW);
        form.readFromObject(object);
        BlobUtils.loadBlobs(form, getBlobManager(), false);
        refreshBlobDownloadHref();

        returnUrl = new UrlBuilder(Locale.getDefault(), Util.getAbsoluteUrl(context.getActionPath()), false)
                .toString();

        returnUrl = appendSearchStringParamIfNecessary(returnUrl);

        if (PageActionLogic.isEmbedded(this)) {
            return getEmbeddedReadView();
        } else {
            return getReadView();
        }
    }

    public Resolution jsonReadData() throws JSONException {
        if (object == null) {
            throw new IllegalStateException("Object not loaded. Are you including the primary key in the URL?");
        }

        setupForm(Mode.VIEW);
        form.readFromObject(object);
        BlobUtils.loadBlobs(form, getBlobManager(), false);
        refreshBlobDownloadHref();
        JSONStringer js = new JSONStringer();
        js.object();
        List<Field> fields = new ArrayList<Field>();
        collectVisibleFields(form, fields);
        fieldsToJson(js, fields);
        js.endObject();
        String jsonText = js.toString();
        return new StreamingResolution(MimeTypes.APPLICATION_JSON_UTF8, jsonText);
    }

    //**************************************************************************
    // Form handling
    //**************************************************************************

    /**
     * Writes the contents of the create or edit form into the persistent object.
     * Assumes that the form has already been validated.
     * Also processes rich-text (HTML) fields by cleaning the submitted HTML according
     * to the {@link #getWhitelist() whitelist}.
     */
    protected void writeFormToObject() {
        form.writeToObject(object);
        for (TextField textField : getEditableRichTextFields()) {
            PropertyAccessor propertyAccessor = textField.getPropertyAccessor();
            String stringValue = textField.getStringValue();
            String cleanText;
            try {
                Whitelist whitelist = getWhitelist();
                cleanText = Jsoup.clean(stringValue, whitelist);
            } catch (Throwable t) {
                logger.error("Could not clean HTML, falling back to escaped text", t);
                cleanText = StringEscapeUtils.escapeHtml(stringValue);
            }
            propertyAccessor.set(object, cleanText);
        }
    }

    /**
     * Returns the JSoup whitelist used to clean user-provided HTML in rich-text fields.
     * @return the default implementation returns the "basic" whitelist ({@see Whitelist#basic()}).
     */
    protected Whitelist getWhitelist() {
        return Whitelist.basic();
    }

    //**************************************************************************
    // Create/Save
    //**************************************************************************

    @Button(list = "crud-search", key = "create.new", order = 1, type = Button.TYPE_SUCCESS, icon = Button.ICON_PLUS
            + Button.ICON_WHITE, group = "crud")
    @RequiresPermissions(permissions = PERMISSION_CREATE)
    public Resolution create() {
        setupForm(Mode.CREATE);
        object = (T) classAccessor.newInstance();
        createSetup(object);
        form.readFromObject(object);

        return getCreateView();
    }

    @Button(list = "crud-create", key = "save", order = 1, type = Button.TYPE_PRIMARY)
    @RequiresPermissions(permissions = PERMISSION_CREATE)
    public Resolution save() {
        setupForm(Mode.CREATE);
        object = (T) classAccessor.newInstance();
        createSetup(object);
        form.readFromObject(object);
        form.readFromRequest(context.getRequest());
        BlobUtils.loadBlobs(form, getTemporaryBlobManager(), false);
        if (form.validate()) {
            writeFormToObject();
            if (createValidate(object)) {
                try {
                    doSave(object);
                    createPostProcess(object);
                    commitTransaction();
                } catch (Throwable e) {
                    String rootCauseMessage = ExceptionUtils.getRootCauseMessage(e);
                    logger.warn(rootCauseMessage, e);
                    SessionMessages.addErrorMessage(rootCauseMessage);
                    saveTemporaryBlobs();
                    return getCreateView();
                }
                //The object on the database was persisted. Now we can save the blobs.
                try {
                    BlobUtils.loadBlobs(form, getTemporaryBlobManager(), true);
                    BlobUtils.saveBlobs(form, getBlobManager());
                } catch (IOException e) {
                    String rootCauseMessage = ExceptionUtils.getRootCauseMessage(e);
                    logger.error("Could not persist blobs!", e);
                    SessionMessages.addErrorMessage(rootCauseMessage);
                }
                if (isPopup()) {
                    popupCloseCallback += "(true)";
                    return new ForwardResolution("/m/crud/popup/close.jsp");
                } else {
                    addSuccessfulSaveInfoMessage();
                    return getSuccessfulSaveView();
                }
            }
        } else {
            saveTemporaryBlobs();
        }
        return getCreateView();
    }

    protected void saveTemporaryBlobs() {
        try {
            BlobUtils.saveBlobs(form, getTemporaryBlobManager());
        } catch (IOException e1) {
            logger.warn("Could not save temporary blobs", e1);
        }
    }

    //**************************************************************************
    // Edit/Update
    //**************************************************************************

    @Buttons({
            @Button(list = "crud-read", key = "edit", order = 1, icon = Button.ICON_EDIT
                    + Button.ICON_WHITE, group = "crud", type = Button.TYPE_SUCCESS),
            @Button(list = "crud-read-default-button", key = "search") })
    @RequiresPermissions(permissions = PERMISSION_EDIT)
    public Resolution edit() {
        setupForm(Mode.EDIT);
        editSetup(object);
        form.readFromObject(object);
        BlobUtils.loadBlobs(form, getBlobManager(), false);
        return getEditView();
    }

    @Button(list = "crud-edit", key = "update", order = 1, type = Button.TYPE_PRIMARY)
    @RequiresPermissions(permissions = PERMISSION_EDIT)
    public Resolution update() {
        setupForm(Mode.EDIT);
        editSetup(object);
        form.readFromObject(object);
        List<Blob> blobsBefore = getBlobsFromForm();
        form.readFromRequest(context.getRequest());
        BlobUtils.loadBlobs(form, getBlobManager(), false);
        BlobUtils.loadBlobs(form, getTemporaryBlobManager(), false);
        if (form.validate()) {
            writeFormToObject();
            if (editValidate(object)) {
                try {
                    doUpdate(object);
                    editPostProcess(object);
                    commitTransaction();
                } catch (Throwable e) {
                    String rootCauseMessage = ExceptionUtils.getRootCauseMessage(e);
                    logger.warn(rootCauseMessage, e);
                    SessionMessages.addErrorMessage(rootCauseMessage);
                    saveTemporaryBlobs();
                    return getEditView();
                }
                try {
                    List<Blob> blobsAfter = getBlobsFromForm();
                    deleteOldBlobs(blobsBefore, blobsAfter);
                    BlobUtils.loadBlobs(form, getTemporaryBlobManager(), true);
                    persistNewBlobs(blobsBefore, blobsAfter);
                } catch (IOException e) {
                    String rootCauseMessage = ExceptionUtils.getRootCauseMessage(e);
                    logger.error("Could not persist blobs!", e);
                    SessionMessages.addErrorMessage(rootCauseMessage);
                }

                SessionMessages.addInfoMessage(ElementsThreadLocals.getText("object.updated.successfully"));
                return getSuccessfulUpdateView();
            }
        } else {
            saveTemporaryBlobs();
        }
        return getEditView();
    }

    protected void persistNewBlobs(List<Blob> blobsBefore, List<Blob> blobsAfter) throws IOException {
        for (FileBlobField field : getBlobFields()) {
            Blob blob = field.getValue();
            if (blobsAfter.contains(blob) && !blobsBefore.contains(blob)) {
                getBlobManager().save(blob);
            }
        }
    }

    protected void deleteOldBlobs(List<Blob> blobsBefore, List<Blob> blobsAfter) {
        List<Blob> toDelete = new ArrayList<Blob>(blobsBefore);
        toDelete.removeAll(blobsAfter);
        for (Blob blob : toDelete) {
            try {
                getBlobManager().delete(blob);
            } catch (IOException e) {
                logger.warn("Could not delete blob: " + blob.getCode(), e);
            }
        }
    }

    //**************************************************************************
    // Bulk Edit/Update
    //**************************************************************************

    public boolean isBulkOperationsEnabled() {
        return (objects != null && !objects.isEmpty()) || "bulkEdit".equals(context.getEventName())
                || "bulkDelete".equals(context.getEventName());
    }

    @Button(list = "crud-search", key = "edit", order = 2, icon = Button.ICON_EDIT, group = "crud")
    @Guard(test = "isBulkOperationsEnabled()", type = GuardType.VISIBLE)
    @RequiresPermissions(permissions = PERMISSION_EDIT)
    public Resolution bulkEdit() {
        if (selection == null || selection.length == 0) {
            SessionMessages.addWarningMessage(ElementsThreadLocals.getText("no.object.was.selected"));
            return new RedirectResolution(returnUrl, false);
        }

        if (selection.length == 1) {
            pk = selection[0].split("/");
            String url = context.getActionPath() + "/" + getPkForUrl(pk);
            url = appendSearchStringParamIfNecessary(url);
            return new RedirectResolution(url).addParameter("returnUrl", returnUrl).addParameter("edit");
        }

        setupForm(Mode.BULK_EDIT);
        disableBlobFields();
        return getBulkEditView();
    }

    /**
     * Handles a bulk update operation (typically invoked by the user submitting a bulk edit form).
     * Note: doesn't handle blobs.
     * @return which view to render next.
     */
    @Button(list = "crud-bulk-edit", key = "update", order = 1, type = Button.TYPE_PRIMARY)
    @RequiresPermissions(permissions = PERMISSION_EDIT)
    public Resolution bulkUpdate() {
        int updated = 0;
        setupForm(Mode.BULK_EDIT);
        disableBlobFields();
        form.readFromRequest(context.getRequest());
        if (form.validate()) {
            for (String current : selection) {
                loadObject(current.split("/"));
                editSetup(object);
                writeFormToObject();
                if (editValidate(object)) {
                    doUpdate(object);
                    editPostProcess(object);
                    updated++;
                }
            }
            try {
                commitTransaction();
            } catch (Throwable e) {
                String rootCauseMessage = ExceptionUtils.getRootCauseMessage(e);
                logger.warn(rootCauseMessage, e);
                SessionMessages.addErrorMessage(rootCauseMessage);
                return getBulkEditView();
            }
            SessionMessages.addInfoMessage(ElementsThreadLocals.getText("update.of._.objects.successful", updated));
            return getSuccessfulUpdateView();
        } else {
            return getBulkEditView();
        }
    }

    //**************************************************************************
    // Delete
    //**************************************************************************

    @Button(list = "crud-read", key = "delete", order = 2, icon = Button.ICON_TRASH, group = "crud")
    @RequiresPermissions(permissions = PERMISSION_DELETE)
    public Resolution delete() {
        if (deleteValidate(object)) {
            doDelete(object);
            try {
                deletePostProcess(object);
                commitTransaction();
                deleteBlobs(object);
                SessionMessages.addInfoMessage(ElementsThreadLocals.getText("object.deleted.successfully"));

                // invalidate the pk on this crud
                pk = null;
            } catch (Exception e) {
                String rootCauseMessage = ExceptionUtils.getRootCauseMessage(e);
                logger.debug(rootCauseMessage, e);
                SessionMessages.addErrorMessage(rootCauseMessage);
            }
        }
        return getSuccessfulDeleteView();
    }

    @Button(list = "crud-search", key = "delete", order = 3, icon = Button.ICON_TRASH, group = "crud")
    @Guard(test = "isBulkOperationsEnabled()", type = GuardType.VISIBLE)
    @RequiresPermissions(permissions = PERMISSION_DELETE)
    public Resolution bulkDelete() {
        int deleted = 0;
        if (selection == null || selection.length == 0) {
            SessionMessages.addWarningMessage(ElementsThreadLocals.getText("no.object.was.selected"));
            return new RedirectResolution(appendSearchStringParamIfNecessary(context.getActionPath())); //TODO why is this different from bulkEdit?
        }
        List<T> objects = new ArrayList<T>(selection.length);
        for (String current : selection) {
            String[] pkArr = current.split("/");
            Serializable pkObject = pkHelper.getPrimaryKey(pkArr);
            T obj = loadObjectByPrimaryKey(pkObject);
            if (deleteValidate(obj)) {
                doDelete(obj);
                deletePostProcess(obj);
                objects.add(obj);
                deleted++;
            }
        }
        try {
            commitTransaction();
            for (T obj : objects) {
                deleteBlobs(obj);
            }
            SessionMessages.addInfoMessage(ElementsThreadLocals.getText("_.objects.deleted.successfully", deleted));
        } catch (Exception e) {
            logger.warn(ExceptionUtils.getRootCauseMessage(e), e);
            SessionMessages.addErrorMessage(ExceptionUtils.getRootCauseMessage(e));
        }

        return getSuccessfulDeleteView();
    }

    //**************************************************************************
    // Hooks/scripting
    //**************************************************************************

    /**
     * Hook method called just after a new object has been created.
     * @param object the new object.
     */
    protected void createSetup(T object) {
    }

    /**
     * Hook method called after values from the create form have been propagated to the new object.
     * @param object the new object.
     * @return true if the object is to be considered valid, false otherwise. In the latter case, the
     * object will not be saved; it is suggested that the cause of the validation failure be displayed
     * to the user (e.g. by using SessionMessages).
     */
    protected boolean createValidate(T object) {
        return true;
    }

    /**
     * Hook method called just before a new object is actually saved to persistent storage.
     * @param object the new object.
     */
    protected void createPostProcess(T object) {
    }

    /**
     * Executes any pending updates on persistent objects. E.g. saves them to the database, or calls the
     * appropriate operation of a web service, etc.
     */
    protected void commitTransaction() {
    }

    /**
     * Hook method called just before an object is used to populate the edit form.
     * @param object the object.
     */
    protected void editSetup(T object) {
    }

    /**
     * Hook method called after values from the edit form have been propagated to the object.
     * @param object the object.
     * @return true if the object is to be considered valid, false otherwise. In the latter case, the
     * object will not be saved; it is suggested that the cause of the validation failure be displayed
     * to the user (e.g. by using SessionMessages).
     */
    protected boolean editValidate(T object) {
        return true;
    }

    /**
     * Hook method called just before an existing object is actually saved to persistent storage.
     * @param object the object just edited.
     */
    protected void editPostProcess(T object) {
    }

    /**
     * Hook method called before an object is deleted.
     * @param object the object.
     * @return true if the delete operation is to be performed, false otherwise. In the latter case,
     * it is suggested that the cause of the validation failure be displayed to the user
     * (e.g. by using SessionMessages).
     */
    protected boolean deleteValidate(T object) {
        return true;
    }

    /**
     * Hook method called just before an object is deleted from persistent storage, but after the doDelete
     * method has been called.
     * @param object the object.
     */
    protected void deletePostProcess(T object) {
    }

    /**
     * Returns the Resolution used to show the Bulk Edit page.
     */
    protected Resolution getBulkEditView() {
        return new ForwardResolution("/m/crud/bulk-edit.jsp");
    }

    /**
     * Returns the Resolution used to show the Create page.
     */
    protected Resolution getCreateView() { //TODO spezzare in popup/non-popup?
        if (isPopup()) {
            return new ForwardResolution("/m/crud/popup/create.jsp");
        } else {
            return new ForwardResolution("/m/crud/create.jsp");
        }
    }

    /**
     * Returns the Resolution used to show the effect of a successful save action.
     * @return by default, a redirect to the returnUrl if present, to the search page otherwise.
     */
    protected Resolution getSuccessfulSaveView() {
        if (StringUtils.isEmpty(returnUrl)) {
            return new RedirectResolution(context.getActionPath());
        } else {
            return new RedirectResolution(returnUrl, false);
        }
    }

    /**
     * Returns the Resolution used to show the effect of a successful update action.
     * @return by default, a redirect to the detail, propagating the search string.
     */
    protected Resolution getSuccessfulUpdateView() {
        return new RedirectResolution(appendSearchStringParamIfNecessary(context.getActionPath()));
    }

    /**
     * Returns the Resolution used to show the effect of a successful update action.
     * @return by default, a redirect to the detail, propagating the search string.
     */
    protected Resolution getSuccessfulDeleteView() {
        return new RedirectResolution(appendSearchStringParamIfNecessary(context.getActionPath()));
    }

    /**
     * Adds an information message (using {@link SessionMessages}) after successful creation of a new record.
     * By default, the message contains a link to the created object as well as a link for creating a new one.
     */
    protected void addSuccessfulSaveInfoMessage() {
        XhtmlBuffer buffer = new XhtmlBuffer();

        pk = pkHelper.generatePkStringArray(object);
        String readUrl = context.getActionPath() + "/" + getPkForUrl(pk);
        String prettyName = ShortNameUtils.getName(getClassAccessor(), object);
        XhtmlBuffer linkToObjectBuffer = new XhtmlBuffer();
        linkToObjectBuffer.writeAnchor(Util.getAbsoluteUrl(readUrl), prettyName);
        buffer.writeNoHtmlEscape(ElementsThreadLocals.getText("object._.saved", linkToObjectBuffer));

        String createUrl = Util.getAbsoluteUrl(context.getActionPath());
        if (!createUrl.contains("?")) {
            createUrl += "?";
        } else {
            createUrl += "&";
        }
        createUrl += "create=";
        createUrl = appendSearchStringParamIfNecessary(createUrl);
        buffer.write(" ");
        buffer.writeAnchor(createUrl, ElementsThreadLocals.getText("create.another.object"));

        SessionMessages.addInfoMessage(buffer);
    }

    /**
     * Returns the Resolution used to show the Edit page.
     */
    protected Resolution getEditView() {
        return new ForwardResolution("/m/crud/edit.jsp");
    }

    /**
     * Returns the Resolution used to show the Read page.
     */
    protected Resolution getReadView() {
        return forwardTo("/m/crud/read.jsp");
    }

    /**
     * Returns the Resolution used to show the Search page when this page is embedded in its parent.
     */
    protected Resolution getEmbeddedReadView() {
        return new ForwardResolution("/m/crud/read.jsp");
    }

    /**
     * Returns the Resolution used to show the Search page.
     */
    protected Resolution getSearchView() {
        return forwardTo("/m/crud/search.jsp");
    }

    /**
     * Returns the Resolution used to show the Search page when this page is embedded in its parent.
     */
    protected Resolution getEmbeddedSearchView() {
        return new ForwardResolution("/m/crud/search.jsp");
    }

    /**
     * Returns the Resolution used to display the search results when paginating or sorting via AJAX.
     */
    protected Resolution getSearchResultsPageView() {
        return new ForwardResolution("/m/crud/datatable.jsp");
    }

    //--------------------------------------------------------------------------
    // Setup
    //--------------------------------------------------------------------------

    public Resolution preparePage() {
        this.crudConfiguration = (CrudConfiguration) pageInstance.getConfiguration();

        if (crudConfiguration == null) {
            logger.warn("Crud is not configured: " + pageInstance.getPath());
            return null;
        }

        ClassAccessor innerAccessor = prepare(pageInstance);
        if (innerAccessor == null) {
            return null;
        }
        classAccessor = new CrudAccessor(crudConfiguration, innerAccessor);
        pkHelper = new PkHelper(classAccessor);

        List<String> parameters = pageInstance.getParameters();
        if (!parameters.isEmpty()) {
            String encoding = getUrlEncoding();
            pk = parameters.toArray(new String[parameters.size()]);
            try {
                for (int i = 0; i < pk.length; i++) {
                    pk[i] = URLDecoder.decode(pk[i], encoding);
                }
            } catch (UnsupportedEncodingException e) {
                throw new Error(e);
            }
            OgnlContext ognlContext = ElementsThreadLocals.getOgnlContext();

            Serializable pkObject;
            try {
                pkObject = pkHelper.getPrimaryKey(pk);
            } catch (Exception e) {
                logger.warn("Invalid primary key", e);
                return notInUseCase(context, parameters);
            }
            object = loadObjectByPrimaryKey(pkObject);
            if (object != null) {
                ognlContext.put(crudConfiguration.getActualVariable(), object);
                String title = getReadTitle();
                pageInstance.setTitle(title);
                pageInstance.setDescription(title);
            } else {
                return notInUseCase(context, parameters);
            }
        } else {
            String title = crudConfiguration.getSearchTitle();
            pageInstance.setTitle(title);
            pageInstance.setDescription(title);
        }
        return null;
    }

    protected Resolution notInUseCase(ActionBeanContext context, List<String> parameters) {
        logger.info("Not in use case: " + crudConfiguration.getName());
        String msg = ElementsThreadLocals.getText("object.not.found._", StringUtils.join(parameters, "/"));
        SessionMessages.addWarningMessage(msg);
        return new ForwardResolution("/m/pageactions/redirect-to-last-working-page.jsp");
    }

    /**
     * <p>Builds the ClassAccessor used to create, manipulate and introspect persistent objects.</p>
     * <p>This method is called during the prepare phase.</p>
     * @param pageInstance the PageInstance corresponding to this action in the current dispatch.
     * @return the ClassAccessor.
     */
    protected abstract ClassAccessor prepare(PageInstance pageInstance);

    public boolean isConfigured() {
        return (classAccessor != null);
    }

    protected void setupPagination() {
        resultSetNavigation = new ResultSetNavigation();
        int position = objects.indexOf(object);
        int size = objects.size();
        resultSetNavigation.setPosition(position);
        resultSetNavigation.setSize(size);
        String baseUrl = calculateBaseSearchUrl();
        if (position >= 0) {
            if (position > 0) {
                resultSetNavigation.setFirstUrl(generateObjectUrl(baseUrl, 0));
                resultSetNavigation.setPreviousUrl(generateObjectUrl(baseUrl, position - 1));
            }
            if (position < size - 1) {
                resultSetNavigation.setLastUrl(generateObjectUrl(baseUrl, size - 1));
                resultSetNavigation.setNextUrl(generateObjectUrl(baseUrl, position + 1));
            }
        }
    }

    protected String calculateBaseSearchUrl() {
        assert pk != null; //Ha senso solo in modalita' read/detail
        String baseUrl = Util.getAbsoluteUrl(context.getActionPath());
        for (int i = 0; i < pk.length; i++) {
            int lastSlashIndex = baseUrl.lastIndexOf('/');
            baseUrl = baseUrl.substring(0, lastSlashIndex);
        }
        return baseUrl;
    }

    protected String generateObjectUrl(String baseUrl, int index) {
        Object o = objects.get(index);
        return generateObjectUrl(baseUrl, o);
    }

    protected String generateObjectUrl(String baseUrl, Object o) {
        String[] objPk = pkHelper.generatePkStringArray(o);
        String url = baseUrl + "/" + getPkForUrl(objPk);
        return new UrlBuilder(Locale.getDefault(), appendSearchStringParamIfNecessary(url), false).toString();
    }

    protected void setupSearchForm() {
        SearchFormBuilder searchFormBuilder = createSearchFormBuilder();
        searchForm = buildSearchForm(configureSearchFormBuilder(searchFormBuilder));

        if (!PageActionLogic.isEmbedded(this)) {
            logger.debug("Search form not embedded, no risk of clashes - reading parameters from request");
            readSearchFormFromRequest();
        }
    }

    protected void readSearchFormFromRequest() {
        if (StringUtils.isBlank(searchString)) {
            searchForm.readFromRequest(context.getRequest());
            searchString = searchForm.toSearchString(getUrlEncoding());
            if (searchString.length() == 0) {
                searchString = null;
            } else {
                searchVisible = true;
            }
        } else {
            MutableHttpServletRequest dummyRequest = new MutableHttpServletRequest();
            String[] parts = searchString.split(",");
            Pattern pattern = Pattern.compile("(.*)=(.*)");
            for (String part : parts) {
                Matcher matcher = pattern.matcher(part);
                if (matcher.matches()) {
                    String key = matcher.group(1);
                    String value = matcher.group(2);
                    logger.debug("Matched part: {}={}", key, value);
                    dummyRequest.addParameter(key, value);
                } else {
                    logger.debug("Could not match part: {}", part);
                }
            }
            searchForm.readFromRequest(dummyRequest);
            searchVisible = true;
        }
    }

    protected SearchFormBuilder createSearchFormBuilder() {
        return new SearchFormBuilder(classAccessor);
    }

    protected SearchFormBuilder configureSearchFormBuilder(SearchFormBuilder searchFormBuilder) {
        // setup option providers
        for (CrudSelectionProvider current : selectionProviderSupport.getCrudSelectionProviders()) {
            SelectionProvider selectionProvider = current.getSelectionProvider();
            if (selectionProvider == null) {
                continue;
            }
            String[] fieldNames = current.getFieldNames();
            searchFormBuilder.configSelectionProvider(selectionProvider, fieldNames);
        }
        return searchFormBuilder.configPrefix(searchPrefix);
    }

    protected SearchForm buildSearchForm(SearchFormBuilder searchFormBuilder) {
        return searchFormBuilder.build();
    }

    protected void setupTableForm(Mode mode) {
        int nRows;
        if (objects == null) {
            nRows = 0;
        } else {
            nRows = objects.size();
        }
        TableFormBuilder tableFormBuilder = createTableFormBuilder();
        configureTableFormBuilder(tableFormBuilder, mode, nRows);
        tableForm = buildTableForm(tableFormBuilder);

        if (objects != null) {
            tableForm.readFromObject(objects);
            refreshTableBlobDownloadHref();
        }
    }

    protected void configureTableFormSelectionProviders(TableFormBuilder tableFormBuilder) {
        // setup option providers
        for (CrudSelectionProvider current : selectionProviderSupport.getCrudSelectionProviders()) {
            SelectionProvider selectionProvider = current.getSelectionProvider();
            if (selectionProvider == null) {
                continue;
            }
            String[] fieldNames = current.getFieldNames();
            tableFormBuilder.configSelectionProvider(selectionProvider, fieldNames);
        }
    }

    protected void configureDetailLink(TableFormBuilder tableFormBuilder) {
        boolean isShowingKey = false;
        for (PropertyAccessor property : classAccessor.getKeyProperties()) {
            if (tableFormBuilder.getPropertyAccessors().contains(property)
                    && tableFormBuilder.isPropertyVisible(property)) {
                isShowingKey = true;
                break;
            }
        }

        String readLinkExpression = getReadLinkExpression();
        OgnlTextFormat hrefFormat = OgnlTextFormat.create(readLinkExpression);
        hrefFormat.setUrl(true);
        String encoding = getUrlEncoding();
        hrefFormat.setEncoding(encoding);

        if (isShowingKey) {
            logger.debug("TableForm: configuring detail links for primary key properties");
            for (PropertyAccessor property : classAccessor.getKeyProperties()) {
                tableFormBuilder.configHrefTextFormat(property.getName(), hrefFormat);
            }
        } else {
            logger.debug("TableForm: configuring detail link for the first visible property");
            for (PropertyAccessor property : classAccessor.getProperties()) {
                if (tableFormBuilder.getPropertyAccessors().contains(property)
                        && tableFormBuilder.isPropertyVisible(property)) {
                    tableFormBuilder.configHrefTextFormat(property.getName(), hrefFormat);
                    break;
                }
            }
        }
    }

    protected void configureSortLinks(TableFormBuilder tableFormBuilder) {
        for (PropertyAccessor propertyAccessor : classAccessor.getProperties()) {
            String propName = propertyAccessor.getName();
            String sortDirection;
            if (propName.equals(sortProperty) && "asc".equals(this.sortDirection)) {
                sortDirection = "desc";
            } else {
                sortDirection = "asc";
            }

            Map<String, Object> parameters = new HashMap<String, Object>();
            parameters.put("sortProperty", propName);
            parameters.put("sortDirection", sortDirection);
            if (!PageActionLogic.isEmbedded(this)) {
                parameters.put(SEARCH_STRING_PARAM, searchString);
            }
            parameters.put(context.getEventName(), "");

            UrlBuilder urlBuilder = new UrlBuilder(Locale.getDefault(),
                    Util.getAbsoluteUrl(context.getActionPath()), false).addParameters(parameters);

            XhtmlBuffer xb = new XhtmlBuffer();
            xb.openElement("a");
            xb.addAttribute("class", "sort-link");
            xb.addAttribute("href", urlBuilder.toString());
            xb.writeNoHtmlEscape("%{label}");
            if (propName.equals(sortProperty)) {
                xb.openElement("i");
                xb.addAttribute("class",
                        "pull-right glyphicon glyphicon-chevron-" + ("desc".equals(sortDirection) ? "up" : "down"));
                xb.closeElement("i");
            }
            xb.closeElement("a");
            OgnlTextFormat hrefFormat = OgnlTextFormat.create(xb.toString());
            String encoding = getUrlEncoding();
            hrefFormat.setEncoding(encoding);
            tableFormBuilder.configHeaderTextFormat(propName, hrefFormat);
        }
    }

    public String getLinkToPage(int page) {
        int rowsPerPage = getCrudConfiguration().getRowsPerPage();
        Map<String, Object> parameters = new HashMap<String, Object>();
        parameters.put("sortProperty", getSortProperty());
        parameters.put("sortDirection", getSortDirection());
        parameters.put("firstResult", page * rowsPerPage);
        parameters.put("maxResults", rowsPerPage);
        if (!PageActionLogic.isEmbedded(this)) {
            parameters.put(AbstractCrudAction.SEARCH_STRING_PARAM, getSearchString());
        }

        UrlBuilder urlBuilder = new UrlBuilder(Locale.getDefault(), Util.getAbsoluteUrl(context.getActionPath()),
                false).addParameters(parameters);
        return urlBuilder.toString();
    }

    protected TableForm buildTableForm(TableFormBuilder tableFormBuilder) {
        TableForm tableForm = tableFormBuilder.build();

        tableForm.setKeyGenerator(pkHelper.createPkGenerator());
        tableForm.setSelectable(true);
        tableForm.setCondensed(true);

        return tableForm;
    }

    protected TableFormBuilder createTableFormBuilder() {
        return new TableFormBuilder(classAccessor);
    }

    /**
     * Configures the builder for the search results form. You can override this method to customize how
     * the form is generated (e.g. adding custom links on specific columns, hiding or showing columns
     * based on some runtime condition, etc.).
     * @param tableFormBuilder the table form builder.
     * @param mode the mode of the form.
     * @param nRows number of rows to display.
     * @return the table form builder.
     */
    protected TableFormBuilder configureTableFormBuilder(TableFormBuilder tableFormBuilder, Mode mode, int nRows) {
        configureTableFormSelectionProviders(tableFormBuilder);
        tableFormBuilder.configPrefix(prefix).configNRows(nRows).configMode(mode);
        if (tableFormBuilder.getPropertyAccessors() == null) {
            tableFormBuilder.configReflectiveFields();
        }

        configureDetailLink(tableFormBuilder);
        configureSortLinks(tableFormBuilder);

        return tableFormBuilder;
    }

    protected void setupForm(Mode mode) {
        FormBuilder formBuilder = createFormBuilder();
        configureFormBuilder(formBuilder, mode);
        form = buildForm(formBuilder);
    }

    protected void configureFormSelectionProviders(FormBuilder formBuilder) {
        // setup option providers
        for (CrudSelectionProvider current : selectionProviderSupport.getCrudSelectionProviders()) {
            SelectionProvider selectionProvider = current.getSelectionProvider();
            if (selectionProvider == null) {
                continue;
            }
            String[] fieldNames = current.getFieldNames();
            if (object != null) {
                Object[] values = new Object[fieldNames.length];
                boolean valuesRead = true;
                for (int i = 0; i < fieldNames.length; i++) {
                    String fieldName = fieldNames[i];
                    try {
                        PropertyAccessor propertyAccessor = classAccessor.getProperty(fieldName);
                        values[i] = propertyAccessor.get(object);
                    } catch (Exception e) {
                        logger.error("Couldn't read property " + fieldName, e);
                        valuesRead = false;
                    }
                }
                if (valuesRead) {
                    selectionProvider.ensureActive(values);
                }
            }
            formBuilder.configSelectionProvider(selectionProvider, fieldNames);
        }
    }

    protected Form buildForm(FormBuilder formBuilder) {
        return formBuilder.build();
    }

    protected void disableBlobFields() {
        //Disable blob fields: we don't support them.
        for (FieldSet fieldSet : form) {
            for (FormElement element : fieldSet) {
                if (element instanceof FileBlobField) {
                    ((FileBlobField) element).setInsertable(false);
                    ((FileBlobField) element).setUpdatable(false);
                }
            }
        }
    }

    protected FormBuilder createFormBuilder() {
        return new FormBuilder(classAccessor);
    }

    /**
     * Configures the builder for the search detail (view, create, edit) form.
     * You can override this method to customize how the form is generated
     * (e.g. adding custom links on specific properties, hiding or showing properties
     * based on some runtime condition, etc.).
     * @param formBuilder the form builder.
     * @param mode the mode of the form.
     * @return the form builder.
     */
    protected FormBuilder configureFormBuilder(FormBuilder formBuilder, Mode mode) {
        formBuilder.configPrefix(prefix).configMode(mode);
        configureFormSelectionProviders(formBuilder);
        return formBuilder;
    }

    //**************************************************************************
    // Return to parent
    //**************************************************************************

    public Resolution returnToSearch() throws Exception {
        if (pk != null) {
            return new RedirectResolution(appendSearchStringParamIfNecessary(calculateBaseSearchUrl()), false);
        } else {
            return new ErrorResolution(500);
        }
    }

    @Override
    @Buttons({ @Button(list = "crud-edit", key = "cancel", order = 99),
            @Button(list = "crud-create", key = "cancel", order = 99),
            @Button(list = "crud-bulk-edit", key = "cancel", order = 99),
            @Button(list = "configuration", key = "cancel", order = 99) })
    public Resolution cancel() {
        if (isPopup()) {
            popupCloseCallback += "(false)";
            return new ForwardResolution("/m/crud/popup/close.jsp");
        } else {
            return super.cancel();
        }
    }

    //--------------------------------------------------------------------------
    // Blob management
    //--------------------------------------------------------------------------

    protected void refreshBlobDownloadHref() {
        for (FieldSet fieldSet : form) {
            for (Field field : fieldSet.fields()) {
                if (field instanceof FileBlobField) {
                    FileBlobField fileBlobField = (FileBlobField) field;
                    Blob blob = fileBlobField.getValue();
                    if (blob != null) {
                        String url = getBlobDownloadUrl(fileBlobField);
                        field.setHref(url);
                    }
                }
            }
        }
    }

    protected void refreshTableBlobDownloadHref() {
        Iterator<?> objIterator = objects.iterator();
        for (TableForm.Row row : tableForm.getRows()) {
            Iterator<Field> fieldIterator = row.iterator();
            Object obj = objIterator.next();
            String baseUrl = null;
            while (fieldIterator.hasNext()) {
                Field field = fieldIterator.next();
                if (field instanceof FileBlobField) {
                    if (baseUrl == null) {
                        String readLinkExpression = getReadLinkExpression();
                        String encoding = getUrlEncoding();
                        OgnlTextFormat hrefFormat = OgnlTextFormat.create(readLinkExpression);
                        hrefFormat.setUrl(true);
                        hrefFormat.setEncoding(encoding);
                        baseUrl = hrefFormat.format(obj);
                    }

                    Blob blob = ((FileBlobField) field).getValue();
                    if (blob != null) {
                        UrlBuilder urlBuilder = new UrlBuilder(Locale.getDefault(), baseUrl, false)
                                .addParameter("downloadBlob", "")
                                .addParameter("propertyName", field.getPropertyAccessor().getName())
                                .addParameter("code", blob.getCode());
                        // although unused, the code parameter makes the url change if the
                        // blob changes. In this way we can ask the browser to cache the url
                        // indefinitely.

                        field.setHref(urlBuilder.toString());
                    }
                }
            }
        }
    }

    public String getBlobDownloadUrl(FileBlobField field) {
        UrlBuilder urlBuilder = new UrlBuilder(Locale.getDefault(), Util.getAbsoluteUrl(context.getActionPath()),
                false).addParameter("downloadBlob", "")
                        .addParameter("propertyName", field.getPropertyAccessor().getName())
                        .addParameter("code", field.getValue().getCode());
        // The code parameter must be kept. See not in refreshTableBlobDownloadHref
        return urlBuilder.toString();
    }

    public Resolution downloadBlob() throws IOException, NoSuchFieldException {
        PropertyAccessor propertyAccessor = classAccessor.getProperty(propertyName);
        String code = (String) propertyAccessor.get(object);
        if (StringUtils.isBlank(code)) {
            return new ErrorResolution(404, "No blob was found");
        }
        BlobManager blobManager = getBlobManager();
        Blob blob = new Blob(code);
        blobManager.loadMetadata(blob);
        long contentLength = blob.getSize();
        String contentType = blob.getContentType();
        String fileName = blob.getFilename();
        long lastModified = blob.getCreateTimestamp().getMillis();
        InputStream inputStream = blobManager.openStream(blob);
        return new StreamingResolution(contentType, inputStream).setFilename(fileName).setLength(contentLength)
                .setLastModified(lastModified);
    }

    protected BlobManager getBlobManager() {
        return blobManager;
    }

    public BlobManager getTemporaryBlobManager() {
        return temporaryBlobManager;
    }

    /**
     * Removes all the file blobs associated with the object from the file system.
     * @param object the persistent object.
     */
    protected void deleteBlobs(T object) {
        List<Blob> blobs = getBlobsFromObject(object);
        for (Blob blob : blobs) {
            try {
                blobManager.delete(blob);
            } catch (IOException e) {
                logger.warn("Could not delete blob: " + blob.getCode(), e);
            }
        }
    }

    protected List<Blob> getBlobsFromObject(T object) {
        List<Blob> blobs = new ArrayList<Blob>();
        for (PropertyAccessor property : classAccessor.getProperties()) {
            if (property.getAnnotation(FileBlob.class) != null) {
                String code = (String) property.get(object);
                if (!StringUtils.isBlank(code)) {
                    blobs.add(new Blob(code));
                }
            }
        }
        return blobs;
    }

    protected List<Blob> getBlobsFromForm() {
        List<Blob> blobs = new ArrayList<Blob>();
        for (FileBlobField blobField : getBlobFields()) {
            if (blobField.getValue() != null) {
                blobs.add(blobField.getValue());
            }
        }
        return blobs;
    }

    protected List<FileBlobField> getBlobFields() {
        List<FileBlobField> blobFields = new ArrayList<FileBlobField>();
        for (FieldSet fieldSet : form) {
            for (FormElement field : fieldSet) {
                if (field instanceof FileBlobField) {
                    blobFields.add((FileBlobField) field);
                }
            }
        }
        return blobFields;
    }

    //**************************************************************************
    // Configuration
    //**************************************************************************

    @Button(list = "pageHeaderButtons", titleKey = "configure", order = 1, icon = Button.ICON_WRENCH)
    @RequiresPermissions(level = AccessLevel.DEVELOP)
    public Resolution configure() {
        prepareConfigurationForms();

        crudConfigurationForm.readFromObject(crudConfiguration);
        if (propertyEdits != null) {
            propertiesTableForm.readFromObject(propertyEdits);
        }

        if (selectionProviderEdits != null) {
            selectionProvidersForm.readFromObject(selectionProviderEdits);
        }

        return getConfigurationView();
    }

    /**
     * Returns the Resolution used to show the configuration page.
     */
    protected abstract Resolution getConfigurationView();

    @Override
    protected void prepareConfigurationForms() {
        super.prepareConfigurationForms();

        setupPropertyEdits();

        if (propertyEdits != null) {
            TableFormBuilder tableFormBuilder = new TableFormBuilder(CrudPropertyEdit.class)
                    .configNRows(propertyEdits.length);
            propertiesTableForm = tableFormBuilder.build();
            propertiesTableForm.setCondensed(true);
        }

        if (selectionProviderSupport != null) {
            Map<List<String>, Collection<String>> selectionProviderNames = selectionProviderSupport
                    .getAvailableSelectionProviderNames();
            if (!selectionProviderNames.isEmpty()) {
                setupSelectionProviderEdits();
                setupSelectionProvidersForm(selectionProviderNames);
            }
        }
    }

    protected void setupSelectionProvidersForm(Map<List<String>, Collection<String>> selectionProviderNames) {
        TableFormBuilder tableFormBuilder = new TableFormBuilder(CrudSelectionProviderEdit.class);
        tableFormBuilder.configNRows(selectionProviderNames.size());
        for (int i = 0; i < selectionProviderEdits.length; i++) {
            Collection<String> availableProviders = selectionProviderNames
                    .get(Arrays.asList(selectionProviderEdits[i].fieldNames));
            if (availableProviders == null || availableProviders.size() == 0) {
                continue;
            }
            DefaultSelectionProvider selectionProvider = new DefaultSelectionProvider(
                    selectionProviderEdits[i].columns);
            selectionProvider.appendRow(null, "None", true);
            for (String spName : availableProviders) {
                selectionProvider.appendRow(spName, spName, true);
            }
            tableFormBuilder.configSelectionProvider(i, selectionProvider, "selectionProvider");
        }
        selectionProvidersForm = tableFormBuilder.build();
        selectionProvidersForm.setCondensed(true);
    }

    protected void setupPropertyEdits() {
        if (classAccessor == null) {
            return;
        }
        PropertyAccessor[] propertyAccessors = classAccessor.getProperties();
        propertyEdits = new CrudPropertyEdit[propertyAccessors.length];
        for (int i = 0; i < propertyAccessors.length; i++) {
            CrudPropertyEdit edit = new CrudPropertyEdit();
            PropertyAccessor propertyAccessor = propertyAccessors[i];
            edit.name = propertyAccessor.getName();
            com.manydesigns.elements.annotations.Label labelAnn = propertyAccessor
                    .getAnnotation(com.manydesigns.elements.annotations.Label.class);
            edit.label = labelAnn != null ? labelAnn.value() : null;
            Enabled enabledAnn = propertyAccessor.getAnnotation(Enabled.class);
            edit.enabled = enabledAnn != null && enabledAnn.value();
            InSummary inSummaryAnn = propertyAccessor.getAnnotation(InSummary.class);
            edit.inSummary = inSummaryAnn != null && inSummaryAnn.value();
            Insertable insertableAnn = propertyAccessor.getAnnotation(Insertable.class);
            edit.insertable = insertableAnn != null && insertableAnn.value();
            Updatable updatableAnn = propertyAccessor.getAnnotation(Updatable.class);
            edit.updatable = updatableAnn != null && updatableAnn.value();
            Searchable searchableAnn = propertyAccessor.getAnnotation(Searchable.class);
            edit.searchable = searchableAnn != null && searchableAnn.value();
            propertyEdits[i] = edit;
        }
    }

    protected void setupSelectionProviderEdits() {
        Map<List<String>, Collection<String>> availableSelectionProviders = selectionProviderSupport
                .getAvailableSelectionProviderNames();
        selectionProviderEdits = new CrudSelectionProviderEdit[availableSelectionProviders.size()];
        int i = 0;
        for (List<String> key : availableSelectionProviders.keySet()) {
            selectionProviderEdits[i] = new CrudSelectionProviderEdit();
            String[] fieldNames = key.toArray(new String[key.size()]);
            selectionProviderEdits[i].fieldNames = fieldNames;
            selectionProviderEdits[i].columns = StringUtils.join(fieldNames, ", ");
            for (CrudSelectionProvider cp : selectionProviderSupport.getCrudSelectionProviders()) {
                if (Arrays.equals(cp.fieldNames, fieldNames)) {
                    SelectionProvider selectionProvider = cp.getSelectionProvider();
                    if (selectionProvider != null) {
                        selectionProviderEdits[i].selectionProvider = selectionProvider.getName();
                        selectionProviderEdits[i].displayMode = selectionProvider.getDisplayMode();
                        selectionProviderEdits[i].searchDisplayMode = selectionProvider.getSearchDisplayMode();
                        selectionProviderEdits[i].createNewHref = cp.getCreateNewValueHref();
                        selectionProviderEdits[i].createNewText = cp.getCreateNewValueText();
                    } else {
                        selectionProviderEdits[i].selectionProvider = null;
                        selectionProviderEdits[i].displayMode = DisplayMode.DROPDOWN;
                        selectionProviderEdits[i].searchDisplayMode = SearchDisplayMode.DROPDOWN;
                    }
                }
            }
            i++;
        }
    }

    @Button(list = "configuration", key = "update.configuration", order = 1, type = Button.TYPE_PRIMARY)
    @RequiresPermissions(level = AccessLevel.DEVELOP)
    public Resolution updateConfiguration() {
        prepareConfigurationForms();

        crudConfigurationForm.readFromObject(crudConfiguration);

        readPageConfigurationFromRequest();

        crudConfigurationForm.readFromRequest(context.getRequest());

        boolean valid = crudConfigurationForm.validate();
        valid = validatePageConfiguration() && valid;

        if (propertiesTableForm != null) {
            propertiesTableForm.readFromObject(propertyEdits);
            propertiesTableForm.readFromRequest(context.getRequest());
            valid = propertiesTableForm.validate() && valid;
        }

        if (selectionProvidersForm != null) {
            selectionProvidersForm.readFromRequest(context.getRequest());
            valid = selectionProvidersForm.validate() && valid;
        }

        if (valid) {
            updatePageConfiguration();
            if (crudConfiguration == null) {
                crudConfiguration = new CrudConfiguration();
            }
            crudConfigurationForm.writeToObject(crudConfiguration);

            if (propertiesTableForm != null) {
                updateProperties();
            }

            if (selectionProviderSupport != null
                    && !selectionProviderSupport.getAvailableSelectionProviderNames().isEmpty()) {
                updateSelectionProviders();
            }

            saveConfiguration(crudConfiguration);

            SessionMessages.addInfoMessage(ElementsThreadLocals.getText("configuration.updated.successfully"));
            return cancel();
        } else {
            SessionMessages.addErrorMessage(ElementsThreadLocals.getText("the.configuration.could.not.be.saved"));
            return getConfigurationView();
        }
    }

    protected void updateSelectionProviders() {
        selectionProvidersForm.writeToObject(selectionProviderEdits);
        crudConfiguration.getSelectionProviders().clear();
        for (CrudSelectionProviderEdit sp : selectionProviderEdits) {
            List<String> key = Arrays.asList(sp.fieldNames);
            if (sp.selectionProvider == null) {
                selectionProviderSupport.disableSelectionProvider(key);
            } else {
                selectionProviderSupport.configureSelectionProvider(key, sp.selectionProvider, sp.displayMode,
                        sp.searchDisplayMode, StringUtils.trimToNull(sp.createNewHref), sp.createNewText);
            }
        }
    }

    protected void updateProperties() {
        propertiesTableForm.writeToObject(propertyEdits);

        List<CrudProperty> newProperties = new ArrayList<CrudProperty>();
        for (CrudPropertyEdit edit : propertyEdits) {
            CrudProperty crudProperty = findProperty(edit.name, crudConfiguration.getProperties());
            if (crudProperty == null) {
                crudProperty = new CrudProperty();
            }

            crudProperty.setName(edit.name);
            crudProperty.setLabel(edit.label);
            crudProperty.setInSummary(edit.inSummary);
            crudProperty.setSearchable(edit.searchable);
            crudProperty.setEnabled(edit.enabled);
            crudProperty.setInsertable(edit.insertable);
            crudProperty.setUpdatable(edit.updatable);

            newProperties.add(crudProperty);
        }
        crudConfiguration.getProperties().clear();
        crudConfiguration.getProperties().addAll(newProperties);
    }

    public boolean isRequiredFieldsPresent() {
        return form.isRequiredFieldsPresent();
    }

    //**************************************************************************
    // Ajax
    //**************************************************************************

    public Resolution jsonSelectFieldOptions() {
        return jsonOptions(prefix, true);
    }

    public Resolution jsonSelectFieldSearchOptions() {
        return jsonOptions(searchPrefix, true);
    }

    public Resolution jsonAutocompleteOptions() {
        return jsonOptions(prefix, false);
    }

    public Resolution jsonAutocompleteSearchOptions() {
        return jsonOptions(searchPrefix, false);
    }

    /**
     * Returns values to update multiple related select fields or a single autocomplete
     * text field, in JSON form.
     * @param prefix form prefix, to read values from the request.
     * @param includeSelectPrompt controls if the first option is a label with no value indicating
     * what field is being selected. For combo boxes you would generally pass true as the value of
     * this parameter; for autocomplete fields, you would likely pass false.
     * @return a Resolution to produce the JSON.
     */
    protected Resolution jsonOptions(String prefix, boolean includeSelectPrompt) {
        CrudSelectionProvider crudSelectionProvider = null;
        for (CrudSelectionProvider current : selectionProviderSupport.getCrudSelectionProviders()) {
            SelectionProvider selectionProvider = current.getSelectionProvider();
            if (selectionProvider.getName().equals(relName)) {
                crudSelectionProvider = current;
                break;
            }
        }
        if (crudSelectionProvider == null) {
            return new ErrorResolution(500);
        }

        SelectionProvider selectionProvider = crudSelectionProvider.getSelectionProvider();
        String[] fieldNames = crudSelectionProvider.getFieldNames();

        Form form = buildForm(createFormBuilder().configFields(fieldNames)
                .configSelectionProvider(selectionProvider, fieldNames).configPrefix(prefix).configMode(Mode.EDIT));

        FieldSet fieldSet = form.get(0);
        //Ensure the value is actually read from the request
        for (Field field : fieldSet.fields()) {
            field.setUpdatable(true);
        }
        form.readFromRequest(context.getRequest());

        SelectField targetField = (SelectField) fieldSet.get(selectionProviderIndex);
        targetField.setLabelSearch(labelSearch);

        String text = targetField.jsonSelectFieldOptions(includeSelectPrompt);
        logger.debug("jsonOptions: {}", text);
        return new StreamingResolution(MimeTypes.APPLICATION_JSON_UTF8, text);
    }

    //--------------------------------------------------------------------------
    // Utilities
    //--------------------------------------------------------------------------

    protected String getUrlEncoding() {
        return portofinoConfiguration.getString(PortofinoProperties.URL_ENCODING,
                PortofinoProperties.URL_ENCODING_DEFAULT);
    }

    /**
     * Searches in a list of properties for a property with a given name.
     * @param name the name of the properties.
     * @param properties the list to search.
     * @return the property with the given name, or null if it couldn't be found.
     */
    protected CrudProperty findProperty(String name, List<CrudProperty> properties) {
        for (CrudProperty p : properties) {
            if (p.getName().equals(name)) {
                return p;
            }
        }
        return null;
    }

    /**
     * Encodes the exploded object indentifier to include it in a URL.
     * @param pk the object identifier as a String array.
     * @return the string to append to the URL.
     */
    protected String getPkForUrl(String[] pk) {
        String encoding = getUrlEncoding();
        try {
            return pkHelper.getPkStringForUrl(pk, encoding);
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }

    /**
     * Returns an OGNL expression that, when evaluated against a persistent object, produces a
     * URL path suitable to be used as a link to that object.
     * @return the read link expression.
     */
    protected String getReadLinkExpression() {
        String actionPath = context.getActionPath();
        StringBuilder sb = new StringBuilder(actionPath);
        if (!actionPath.endsWith("/")) {
            sb.append("/");
        }
        boolean first = true;

        for (PropertyAccessor property : classAccessor.getKeyProperties()) {
            if (first) {
                first = false;
            } else {
                sb.append("/");
            }
            sb.append("%{");
            sb.append(property.getName());
            sb.append("}");
        }
        appendSearchStringParamIfNecessary(sb);
        return sb.toString();
    }

    /**
     * If a search has been executed, appends a URL-encoded String representation of the search criteria
     * to the given string, as a GET parameter.
     * @param s the base string.
     * @return the base string with the search criteria appended
     */
    protected String appendSearchStringParamIfNecessary(String s) {
        return appendSearchStringParamIfNecessary(new StringBuilder(s)).toString();
    }

    /**
     * If a search has been executed, appends a URL-encoded String representation of the search criteria
     * to the given StringBuilder, as a GET parameter. The StringBuilder's contents are modified.
     * @param sb the base string.
     * @return sb.
     */
    protected StringBuilder appendSearchStringParamIfNecessary(StringBuilder sb) {
        String searchStringParam = getEncodedSearchStringParam();
        if (searchStringParam != null) {
            if (sb.indexOf("?") == -1) {
                sb.append('?');
            } else {
                sb.append('&');
            }
            sb.append(searchStringParam);
        }
        return sb;
    }

    /**
     * Encodes the current search string (a representation of the current search criteria as a series of GET
     * parameters) to an URL-encoded GET parameter.
     * @return the encoded search string.
     */
    protected String getEncodedSearchStringParam() {
        if (StringUtils.isBlank(searchString)) {
            return null;
        }
        String encodedSearchString = "searchString=";
        try {
            String encoding = getUrlEncoding();
            String encoded = URLEncoder.encode(searchString, encoding);
            if (searchString.equals(URLDecoder.decode(encoded, encoding))) {
                encodedSearchString += encoded;
            } else {
                logger.warn("Could not encode search string \"" + StringEscapeUtils.escapeJava(searchString)
                        + "\" with encoding " + encoding);
                return null;
            }
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
        return encodedSearchString;
    }

    /**
     * Writes a collection of fields as properties of a JSON object.
     * @param js the JSONStringer to write to. Must have a JSON object open for writing.
     * @param fields the fields to output
     * @throws JSONException if the JSON can not be generated.
     */
    protected void fieldsToJson(JSONStringer js, Collection<Field> fields) throws JSONException {
        for (Field field : fields) {
            Object value = field.getValue();
            String displayValue = field.getDisplayValue();
            String href = field.getHref();
            js.key(field.getPropertyAccessor().getName());
            js.object().key("value").value(value).key("displayValue").value(displayValue).key("href").value(href)
                    .endObject();
        }
    }

    protected List<Field> collectVisibleFields(Form form, List<Field> fields) {
        for (FieldSet fieldSet : form) {
            collectVisibleFields(fieldSet, fields);
        }
        return fields;
    }

    protected List<Field> collectVisibleFields(FieldSet fieldSet, List<Field> fields) {
        for (FormElement element : fieldSet) {
            if (element instanceof Field) {
                Field field = (Field) element;
                if (field.isEnabled()) {
                    fields.add(field);
                }
            } else if (element instanceof FieldSet) {
                collectVisibleFields((FieldSet) element, fields);
            }
        }
        return fields;
    }

    //--------------------------------------------------------------------------
    // Accessors
    //--------------------------------------------------------------------------

    public String getReadTitle() {
        String title = crudConfiguration.getReadTitle();
        if (StringUtils.isEmpty(title)) {
            return ShortNameUtils.getName(getClassAccessor(), object);
        } else {
            OgnlTextFormat textFormat = OgnlTextFormat.create(title);
            return textFormat.format(this);
        }
    }

    public String getSearchTitle() {
        String title = crudConfiguration.getSearchTitle();
        if (StringUtils.isBlank(title)) {
            title = getPage().getTitle();
        }
        OgnlTextFormat textFormat = OgnlTextFormat.create(StringUtils.defaultString(title));
        return textFormat.format(this);
    }

    public String getEditTitle() {
        String title = crudConfiguration.getEditTitle();
        if (StringUtils.isEmpty(title)) {
            return ShortNameUtils.getName(getClassAccessor(), object);
        } else {
            OgnlTextFormat textFormat = OgnlTextFormat.create(StringUtils.defaultString(title));
            return textFormat.format(this);
        }
    }

    public String getCreateTitle() {
        String title = crudConfiguration.getCreateTitle();
        if (StringUtils.isBlank(title)) {
            title = getPage().getTitle();
        }
        OgnlTextFormat textFormat = OgnlTextFormat.create(StringUtils.defaultString(title));
        return textFormat.format(this);
    }

    public CrudConfiguration getCrudConfiguration() {
        return crudConfiguration;
    }

    public void setCrudConfiguration(CrudConfiguration crudConfiguration) {
        this.crudConfiguration = crudConfiguration;
    }

    public ClassAccessor getClassAccessor() {
        return classAccessor;
    }

    public void setClassAccessor(ClassAccessor classAccessor) {
        this.classAccessor = classAccessor;
    }

    public PkHelper getPkHelper() {
        return pkHelper;
    }

    public void setPkHelper(PkHelper pkHelper) {
        this.pkHelper = pkHelper;
    }

    public List<CrudSelectionProvider> getCrudSelectionProviders() {
        return selectionProviderSupport.getCrudSelectionProviders();
    }

    public String[] getSelection() {
        return selection;
    }

    public void setSelection(String[] selection) {
        this.selection = selection;
    }

    public String getSearchString() {
        return searchString;
    }

    public void setSearchString(String searchString) {
        this.searchString = searchString;
    }

    public String getSuccessReturnUrl() {
        return successReturnUrl;
    }

    public void setSuccessReturnUrl(String successReturnUrl) {
        this.successReturnUrl = successReturnUrl;
    }

    public SearchForm getSearchForm() {
        return searchForm;
    }

    public void setSearchForm(SearchForm searchForm) {
        this.searchForm = searchForm;
    }

    public List<? extends T> getObjects() {
        return objects;
    }

    public void setObjects(List<? extends T> objects) {
        this.objects = objects;
    }

    public T getObject() {
        return object;
    }

    public void setObject(T object) {
        this.object = object;
    }

    public boolean isMultipartRequest() {
        return form != null && form.isMultipartRequest();
    }

    public List<TextField> getEditableRichTextFields() {
        List<TextField> richTextFields = new ArrayList<TextField>();
        for (FieldSet fieldSet : form) {
            for (FormElement field : fieldSet) {
                if (field instanceof TextField
                        && ((TextField) field).isEnabled() && !form.getMode()
                                .isView(((TextField) field).isInsertable(), ((TextField) field).isUpdatable())
                        && ((TextField) field).isRichText()) {
                    richTextFields.add(((TextField) field));
                }
            }
        }
        return richTextFields;
    }

    public boolean isFormWithRichTextFields() {
        return !getEditableRichTextFields().isEmpty();
    }

    public Form getCrudConfigurationForm() {
        return crudConfigurationForm;
    }

    public void setCrudConfigurationForm(Form crudConfigurationForm) {
        this.crudConfigurationForm = crudConfigurationForm;
    }

    public TableForm getPropertiesTableForm() {
        return propertiesTableForm;
    }

    public Form getForm() {
        return form;
    }

    public void setForm(Form form) {
        this.form = form;
    }

    public TableForm getTableForm() {
        return tableForm;
    }

    public void setTableForm(TableForm tableForm) {
        this.tableForm = tableForm;
    }

    public TableForm getSelectionProvidersForm() {
        return selectionProvidersForm;
    }

    public Integer getFirstResult() {
        return firstResult;
    }

    public void setFirstResult(Integer firstResult) {
        this.firstResult = firstResult;
    }

    public Integer getMaxResults() {
        return maxResults;
    }

    public void setMaxResults(Integer maxResults) {
        this.maxResults = maxResults;
    }

    public String getSortProperty() {
        return sortProperty;
    }

    public void setSortProperty(String sortProperty) {
        this.sortProperty = sortProperty;
    }

    public String getSortDirection() {
        return sortDirection;
    }

    public void setSortDirection(String sortDirection) {
        this.sortDirection = sortDirection;
    }

    public String getPropertyName() {
        return propertyName;
    }

    public void setPropertyName(String propertyName) {
        this.propertyName = propertyName;
    }

    public boolean isSearchVisible() {
        //If embedded, search is always closed by default
        return searchVisible && !PageActionLogic.isEmbedded(this);
    }

    public void setSearchVisible(boolean searchVisible) {
        this.searchVisible = searchVisible;
    }

    public String getRelName() {
        return relName;
    }

    public void setRelName(String relName) {
        this.relName = relName;
    }

    public int getSelectionProviderIndex() {
        return selectionProviderIndex;
    }

    public void setSelectionProviderIndex(int selectionProviderIndex) {
        this.selectionProviderIndex = selectionProviderIndex;
    }

    public String getSelectFieldMode() {
        return selectFieldMode;
    }

    public void setSelectFieldMode(String selectFieldMode) {
        this.selectFieldMode = selectFieldMode;
    }

    public String getLabelSearch() {
        return labelSearch;
    }

    public void setLabelSearch(String labelSearch) {
        this.labelSearch = labelSearch;
    }

    public boolean isPopup() {
        return !StringUtils.isEmpty(popupCloseCallback);
    }

    public String getPopupCloseCallback() {
        return popupCloseCallback;
    }

    public void setPopupCloseCallback(String popupCloseCallback) {
        this.popupCloseCallback = popupCloseCallback;
    }

    public ResultSetNavigation getResultSetNavigation() {
        return resultSetNavigation;
    }

    public void setResultSetNavigation(ResultSetNavigation resultSetNavigation) {
        this.resultSetNavigation = resultSetNavigation;
    }

}