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