001/* 002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Anahide Tchertchian 016 */ 017 018package org.nuxeo.ecm.platform.ui.web.component.list; 019 020import java.io.IOException; 021import java.io.StringWriter; 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027 028import javax.faces.application.FacesMessage; 029import javax.faces.component.NamingContainer; 030import javax.faces.component.UIComponent; 031import javax.faces.context.FacesContext; 032import javax.faces.context.ResponseWriter; 033import javax.faces.event.PhaseId; 034 035import org.apache.commons.lang.StringUtils; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.ecm.platform.ui.web.model.EditableModel; 039import org.nuxeo.ecm.platform.ui.web.tag.fn.Functions; 040 041import com.sun.faces.facelets.tag.jsf.ComponentSupport; 042 043/** 044 * Editable list component, relying on client side javascript code to handle adding/removing element from the target 045 * list. 046 * 047 * @since 7.2 048 */ 049public class UIJavascriptList extends UIEditableList { 050 051 public static final String COMPONENT_TYPE = UIJavascriptList.class.getName(); 052 053 public static final String COMPONENT_FAMILY = UIJavascriptList.class.getName(); 054 055 private static final Log log = LogFactory.getLog(UIJavascriptList.class); 056 057 protected static final String TEMPLATE_INDEX_MARKER = "TEMPLATE_INDEX_MARKER"; 058 059 protected static final String ROW_INDEXES_PARAM = "rowIndex[]"; 060 061 protected static final String IS_LIST_TEMPLATE_VAR = "isListTemplate"; 062 063 protected enum PropertyKeys { 064 rowIndexes; 065 } 066 067 public void setRowIndexes(int[] rowIndexes) { 068 getStateHelper().put(PropertyKeys.rowIndexes, rowIndexes); 069 } 070 071 public int[] getRowIndexes() { 072 return (int[]) getStateHelper().eval(PropertyKeys.rowIndexes); 073 } 074 075 @Override 076 public String getFamily() { 077 return COMPONENT_FAMILY; 078 } 079 080 /** 081 * Override container client id resolution to handle recursion. 082 */ 083 @Override 084 @SuppressWarnings("deprecation") 085 public String getContainerClientId(FacesContext context) { 086 String id = super.getClientId(context); 087 int index = getRowIndex(); 088 if (index == -2) { 089 id += SEPARATOR_CHAR + TEMPLATE_INDEX_MARKER; 090 } else if (index != -1) { 091 id += SEPARATOR_CHAR + String.valueOf(index); 092 } 093 return id; 094 } 095 096 /** 097 * Renders an element using rowIndex -2 and client side marker {@link #TEMPLATE_INDEX_MARKER}. 098 * <p> 099 * This element will be used on client side by js code to handle addition of a new element. 100 */ 101 protected void encodeTemplate(FacesContext context) throws IOException { 102 int oldIndex = getRowIndex(); 103 Object requestMapValue = saveRequestMapModelValue(); 104 Map<String, Object> requestMap = getFacesContext().getExternalContext().getRequestMap(); 105 boolean hasVar = false; 106 if (requestMap.containsKey(IS_LIST_TEMPLATE_VAR)) { 107 hasVar = true; 108 } 109 Object oldIsTemplateBoolean = requestMap.remove(IS_LIST_TEMPLATE_VAR); 110 111 try { 112 setRowIndex(-2); 113 114 // expose a boolean that can be used on client side to hide this element without disturbing the DOM 115 requestMap.put(IS_LIST_TEMPLATE_VAR, Boolean.TRUE); 116 117 // render the template as escaped html 118 ResponseWriter oldResponseWriter = context.getResponseWriter(); 119 StringWriter cacheingWriter = new StringWriter(); 120 121 ResponseWriter newResponseWriter = context.getResponseWriter().cloneWithWriter(cacheingWriter); 122 123 context.setResponseWriter(newResponseWriter); 124 125 if (getChildCount() > 0) { 126 for (UIComponent kid : getChildren()) { 127 if (!kid.isRendered()) { 128 continue; 129 } 130 try { 131 ComponentSupport.encodeRecursive(context, kid); 132 } catch (IOException err) { 133 log.error("Error while rendering component " + kid); 134 } 135 } 136 } 137 138 cacheingWriter.flush(); 139 cacheingWriter.close(); 140 141 context.setResponseWriter(oldResponseWriter); 142 143 String html = Functions.htmlEscape(cacheingWriter.toString()); 144 ResponseWriter writer = context.getResponseWriter(); 145 writer.write("<script type=\"text/x-html-template\">"); 146 writer.write(html); 147 writer.write("</script>"); 148 149 } finally { 150 setRowIndex(oldIndex); 151 152 // restore 153 if (hasVar) { 154 requestMap.put(IS_LIST_TEMPLATE_VAR, oldIsTemplateBoolean); 155 } else { 156 requestMap.remove(IS_LIST_TEMPLATE_VAR); 157 } 158 restoreRequestMapModelValue(requestMapValue); 159 } 160 } 161 162 @Override 163 @SuppressWarnings("deprecation") 164 public void decode(FacesContext context) { 165 super.decode(context); 166 Map<String, String[]> requestMap = context.getExternalContext().getRequestParameterValuesMap(); 167 168 String clientId = getClientId() + NamingContainer.SEPARATOR_CHAR + ROW_INDEXES_PARAM; 169 String[] v = requestMap.get(clientId); 170 if (v == null) { 171 // no info => no elements to decode 172 setRowIndexes(null); 173 return; 174 } 175 176 try { 177 int[] indexes = new int[v.length]; 178 for (int i = 0; i < indexes.length; i++) { 179 indexes[i] = Integer.valueOf(v[i]); 180 } 181 setRowIndexes(indexes); 182 } catch (NumberFormatException e) { 183 throw new IllegalArgumentException(String.format("Invalid value '%s' for row indexes at '%s'", 184 StringUtils.join(v, ","), clientId)); 185 } 186 } 187 188 protected void processFacetsAndChildren(final FacesContext context, final PhaseId phaseId) { 189 List<UIComponent> stamps = getChildren(); 190 EditableModel model = getEditableModel(); 191 int oldIndex = getRowIndex(); 192 int[] rowIndexes = getRowIndexes(); 193 Object requestMapValue = saveRequestMapModelValue(); 194 195 try { 196 197 if (phaseId == PhaseId.APPLY_REQUEST_VALUES && rowIndexes != null) { 198 for (int i = 0; i < rowIndexes.length; i++) { 199 int idx = rowIndexes[i]; 200 setRowIndex(idx); 201 if (!isRowAvailable()) { 202 // new value => insert it, initialized with template 203 model.insertValue(idx, getEditableModel().getUnreferencedTemplate()); 204 } 205 } 206 } 207 208 List<Integer> deletedIndexes = new ArrayList<Integer>(); 209 if (phaseId == PhaseId.PROCESS_VALIDATIONS) { 210 // check deleted indexes, to avoid performing validation on them 211 // A map with the new index for each row key 212 Map<Integer, Integer> keyIndexMap = new HashMap<>(); 213 if (rowIndexes != null) { 214 for (int i = 0; i < rowIndexes.length; i++) { 215 int idx = rowIndexes[i]; 216 keyIndexMap.put(idx, i); 217 } 218 } 219 for (int i = 0; i < getRowCount(); i++) { 220 // This row has been deleted 221 if (!keyIndexMap.containsKey(i)) { 222 deletedIndexes.add(i); 223 } 224 } 225 } 226 227 int end = getRowCount(); 228 for (int idx = 0; idx < end; idx++) { 229 if (deletedIndexes.contains(idx)) { 230 continue; 231 } 232 setRowIndex(idx); 233 if (isRowAvailable()) { 234 for (UIComponent stamp : stamps) { 235 processComponent(context, stamp, phaseId); 236 } 237 if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { 238 // detect changes during process update phase and fill 239 // the EditableModel list diff. 240 if (isRowModified()) { 241 recordValueModified(idx, getRowData()); 242 } 243 } 244 } else { 245 break; 246 } 247 } 248 249 if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { 250 // A map with the new index for each row key 251 Map<Integer, Integer> keyIndexMap = new HashMap<>(); 252 if (rowIndexes != null) { 253 for (int i = 0; i < rowIndexes.length; i++) { 254 int idx = rowIndexes[i]; 255 keyIndexMap.put(idx, i); 256 } 257 } 258 259 // rows to delete 260 List<Integer> toDelete = new ArrayList<>(); 261 // client id 262 String cid = super.getClientId(context); 263 264 // move rows 265 for (int i = 0; i < getRowCount(); i++) { 266 setRowKey(i); 267 int curIdx = getRowIndex(); 268 269 // This row has been deleted 270 if (!keyIndexMap.containsKey(i)) { 271 toDelete.add(i); 272 } else { // This row has been moved 273 int newIdx = keyIndexMap.get(i); 274 if (curIdx != newIdx) { 275 model.moveValue(curIdx, newIdx); 276 // also move any messages in the context attached to the old index 277 String prefix = cid + SEPARATOR_CHAR + curIdx + SEPARATOR_CHAR; 278 String replacement = cid + SEPARATOR_CHAR + newIdx + SEPARATOR_CHAR; 279 Iterator<String> it = context.getClientIdsWithMessages(); 280 while (it.hasNext()) { 281 String id = it.next(); 282 if (id != null && id.startsWith(prefix)) { 283 Iterator<FacesMessage> mit = context.getMessages(id); 284 while (mit.hasNext()) { 285 context.addMessage(id.replaceFirst(prefix, replacement), mit.next()); 286 } 287 } 288 } 289 } 290 } 291 } 292 293 // delete rows 294 for (int i : toDelete) { 295 setRowKey(i); 296 model.removeValue(getRowIndex()); 297 } 298 } 299 300 } finally { 301 setRowIndex(oldIndex); 302 restoreRequestMapModelValue(requestMapValue); 303 } 304 } 305 306}