/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Anahide Tchertchian
*/
package org.nuxeo.ecm.platform.ui.web.component.list;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.faces.application.FacesMessage;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.PhaseId;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.ui.web.model.EditableModel;
import org.nuxeo.ecm.platform.ui.web.tag.fn.Functions;
import com.sun.faces.facelets.tag.jsf.ComponentSupport;
/**
* Editable list component, relying on client side javascript code to handle adding/removing element from the target
* list.
*
* @since 7.2
*/
public class UIJavascriptList extends UIEditableList {
public static final String COMPONENT_TYPE = UIJavascriptList.class.getName();
public static final String COMPONENT_FAMILY = UIJavascriptList.class.getName();
private static final Log log = LogFactory.getLog(UIJavascriptList.class);
protected static final String TEMPLATE_INDEX_MARKER = "TEMPLATE_INDEX_MARKER";
protected static final String ROW_INDEXES_PARAM = "rowIndex[]";
protected static final String IS_LIST_TEMPLATE_VAR = "isListTemplate";
protected enum PropertyKeys {
rowIndexes;
}
public void setRowIndexes(int[] rowIndexes) {
getStateHelper().put(PropertyKeys.rowIndexes, rowIndexes);
}
public int[] getRowIndexes() {
return (int[]) getStateHelper().eval(PropertyKeys.rowIndexes);
}
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
/**
* Override container client id resolution to handle recursion.
*/
@Override
@SuppressWarnings("deprecation")
public String getContainerClientId(FacesContext context) {
String id = super.getClientId(context);
int index = getRowIndex();
if (index == -2) {
id += SEPARATOR_CHAR + TEMPLATE_INDEX_MARKER;
} else if (index != -1) {
id += SEPARATOR_CHAR + String.valueOf(index);
}
return id;
}
/**
* Renders an element using rowIndex -2 and client side marker {@link #TEMPLATE_INDEX_MARKER}.
* <p>
* This element will be used on client side by js code to handle addition of a new element.
*/
protected void encodeTemplate(FacesContext context) throws IOException {
int oldIndex = getRowIndex();
Object requestMapValue = saveRequestMapModelValue();
Map<String, Object> requestMap = getFacesContext().getExternalContext().getRequestMap();
boolean hasVar = false;
if (requestMap.containsKey(IS_LIST_TEMPLATE_VAR)) {
hasVar = true;
}
Object oldIsTemplateBoolean = requestMap.remove(IS_LIST_TEMPLATE_VAR);
try {
setRowIndex(-2);
// expose a boolean that can be used on client side to hide this element without disturbing the DOM
requestMap.put(IS_LIST_TEMPLATE_VAR, Boolean.TRUE);
// render the template as escaped html
ResponseWriter oldResponseWriter = context.getResponseWriter();
StringWriter cacheingWriter = new StringWriter();
ResponseWriter newResponseWriter = context.getResponseWriter().cloneWithWriter(cacheingWriter);
context.setResponseWriter(newResponseWriter);
if (getChildCount() > 0) {
for (UIComponent kid : getChildren()) {
if (!kid.isRendered()) {
continue;
}
try {
ComponentSupport.encodeRecursive(context, kid);
} catch (IOException err) {
log.error("Error while rendering component " + kid);
}
}
}
cacheingWriter.flush();
cacheingWriter.close();
context.setResponseWriter(oldResponseWriter);
String html = Functions.htmlEscape(cacheingWriter.toString());
ResponseWriter writer = context.getResponseWriter();
writer.write("<script type=\"text/x-html-template\">");
writer.write(html);
writer.write("</script>");
} finally {
setRowIndex(oldIndex);
// restore
if (hasVar) {
requestMap.put(IS_LIST_TEMPLATE_VAR, oldIsTemplateBoolean);
} else {
requestMap.remove(IS_LIST_TEMPLATE_VAR);
}
restoreRequestMapModelValue(requestMapValue);
}
}
@Override
@SuppressWarnings("deprecation")
public void decode(FacesContext context) {
super.decode(context);
Map<String, String[]> requestMap = context.getExternalContext().getRequestParameterValuesMap();
String clientId = getClientId() + NamingContainer.SEPARATOR_CHAR + ROW_INDEXES_PARAM;
String[] v = requestMap.get(clientId);
if (v == null) {
// no info => no elements to decode
setRowIndexes(null);
return;
}
try {
int[] indexes = new int[v.length];
for (int i = 0; i < indexes.length; i++) {
indexes[i] = Integer.valueOf(v[i]);
}
setRowIndexes(indexes);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("Invalid value '%s' for row indexes at '%s'",
StringUtils.join(v, ","), clientId));
}
}
protected void processFacetsAndChildren(final FacesContext context, final PhaseId phaseId) {
List<UIComponent> stamps = getChildren();
EditableModel model = getEditableModel();
int oldIndex = getRowIndex();
int[] rowIndexes = getRowIndexes();
Object requestMapValue = saveRequestMapModelValue();
try {
if (phaseId == PhaseId.APPLY_REQUEST_VALUES && rowIndexes != null) {
for (int i = 0; i < rowIndexes.length; i++) {
int idx = rowIndexes[i];
setRowIndex(idx);
if (!isRowAvailable()) {
// new value => insert it, initialized with template
model.insertValue(idx, getEditableModel().getUnreferencedTemplate());
}
}
}
List<Integer> deletedIndexes = new ArrayList<Integer>();
if (phaseId == PhaseId.PROCESS_VALIDATIONS) {
// check deleted indexes, to avoid performing validation on them
// A map with the new index for each row key
Map<Integer, Integer> keyIndexMap = new HashMap<>();
if (rowIndexes != null) {
for (int i = 0; i < rowIndexes.length; i++) {
int idx = rowIndexes[i];
keyIndexMap.put(idx, i);
}
}
for (int i = 0; i < getRowCount(); i++) {
// This row has been deleted
if (!keyIndexMap.containsKey(i)) {
deletedIndexes.add(i);
}
}
}
int end = getRowCount();
for (int idx = 0; idx < end; idx++) {
if (deletedIndexes.contains(idx)) {
continue;
}
setRowIndex(idx);
if (isRowAvailable()) {
for (UIComponent stamp : stamps) {
processComponent(context, stamp, phaseId);
}
if (phaseId == PhaseId.UPDATE_MODEL_VALUES) {
// detect changes during process update phase and fill
// the EditableModel list diff.
if (isRowModified()) {
recordValueModified(idx, getRowData());
}
}
} else {
break;
}
}
if (phaseId == PhaseId.UPDATE_MODEL_VALUES) {
// A map with the new index for each row key
Map<Integer, Integer> keyIndexMap = new HashMap<>();
if (rowIndexes != null) {
for (int i = 0; i < rowIndexes.length; i++) {
int idx = rowIndexes[i];
keyIndexMap.put(idx, i);
}
}
// rows to delete
List<Integer> toDelete = new ArrayList<>();
// client id
String cid = super.getClientId(context);
// move rows
for (int i = 0; i < getRowCount(); i++) {
setRowKey(i);
int curIdx = getRowIndex();
// This row has been deleted
if (!keyIndexMap.containsKey(i)) {
toDelete.add(i);
} else { // This row has been moved
int newIdx = keyIndexMap.get(i);
if (curIdx != newIdx) {
model.moveValue(curIdx, newIdx);
// also move any messages in the context attached to the old index
String prefix = cid + SEPARATOR_CHAR + curIdx + SEPARATOR_CHAR;
String replacement = cid + SEPARATOR_CHAR + newIdx + SEPARATOR_CHAR;
Iterator<String> it = context.getClientIdsWithMessages();
while (it.hasNext()) {
String id = it.next();
if (id != null && id.startsWith(prefix)) {
Iterator<FacesMessage> mit = context.getMessages(id);
while (mit.hasNext()) {
context.addMessage(id.replaceFirst(prefix, replacement), mit.next());
}
}
}
}
}
}
// delete rows
for (int i : toDelete) {
setRowKey(i);
model.removeValue(getRowIndex());
}
}
} finally {
setRowIndex(oldIndex);
restoreRequestMapModelValue(requestMapValue);
}
}
}