001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.xml.xml2json.renderer; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.ade.configuration.CmsFormatterUtils; 032import org.opencms.ade.containerpage.shared.CmsFormatterConfig; 033import org.opencms.file.CmsFile; 034import org.opencms.file.CmsObject; 035import org.opencms.file.CmsResource; 036import org.opencms.json.JSONArray; 037import org.opencms.json.JSONException; 038import org.opencms.json.JSONObject; 039import org.opencms.main.CmsException; 040import org.opencms.main.CmsLog; 041import org.opencms.main.OpenCms; 042import org.opencms.util.CmsStringUtil; 043import org.opencms.util.CmsUUID; 044import org.opencms.xml.containerpage.CmsContainerBean; 045import org.opencms.xml.containerpage.CmsContainerElementBean; 046import org.opencms.xml.containerpage.CmsContainerPageBean; 047import org.opencms.xml.containerpage.CmsXmlContainerPage; 048import org.opencms.xml.containerpage.CmsXmlContainerPageFactory; 049import org.opencms.xml.containerpage.I_CmsFormatterBean; 050 051import java.util.ArrayList; 052import java.util.Collection; 053import java.util.Collections; 054import java.util.HashMap; 055import java.util.List; 056import java.util.Locale; 057import java.util.Map; 058import java.util.function.Predicate; 059 060import org.apache.commons.logging.Log; 061 062/** 063 * Used for rendering container pages as a JSON structure. 064 */ 065public class CmsJsonRendererContainerPage { 066 067 /** 068 * Tree node wrapper for a container. 069 */ 070 public class ContainerNode { 071 072 /** The container bean. */ 073 private CmsContainerBean m_container; 074 075 /** List of nodes corresponding to container elements. */ 076 private List<ElementNode> m_elements = new ArrayList<>(); 077 078 /** 079 * Creates a new node for the given container. 080 * 081 * @param container the container bean 082 */ 083 public ContainerNode(CmsContainerBean container) { 084 085 m_container = container; 086 } 087 088 /** 089 * Adds a container element subnode. 090 * 091 * @param elemNode the container element node 092 */ 093 public void add(ElementNode elemNode) { 094 095 m_elements.add(elemNode); 096 } 097 098 /** 099 * Gets the container bean. 100 * 101 * @return the container bean 102 */ 103 public CmsContainerBean getContainer() { 104 105 return m_container; 106 } 107 108 /** 109 * Gets the nodes corresponding to the container elements. 110 * 111 * @return the nodes for the container elements 112 */ 113 public List<ElementNode> getElements() { 114 115 return Collections.unmodifiableList(m_elements); 116 } 117 118 /** 119 * Gets the container name. 120 * 121 * @return the container name 122 */ 123 public String getName() { 124 125 return m_container.getName(); 126 } 127 128 /** 129 * Returns the container type. 130 * 131 * @return the container type 132 */ 133 public String getType() { 134 135 return m_container.getType(); 136 } 137 138 /** 139 * Returns whether this container is a detail only container. 140 * 141 * @return whether a detail only container or not 142 */ 143 public boolean isDetailOnlyContainer() { 144 145 return m_container.isDetailOnly(); 146 } 147 148 /** 149 * Returns whether this container is a nested container. 150 * 151 * @return whether a nested container or not 152 */ 153 public boolean isNestedContainer() { 154 155 return m_container.isNestedContainer(); 156 } 157 158 /** 159 * Returns whether this container is a root container. 160 * 161 * @return whether a root container or not 162 */ 163 public boolean isRootContainer() { 164 165 return m_container.isRootContainer(); 166 } 167 } 168 169 /** 170 * Tree node wrapper around a container element. 171 */ 172 public class ElementNode { 173 174 /** The map of sub-containers by name. */ 175 private Map<String, ContainerNode> m_containers = new HashMap<>(); 176 177 /** The element. */ 178 private CmsContainerElementBean m_element; 179 180 /** The parent container. */ 181 private ContainerNode m_parentContainerNode; 182 183 /** 184 * Creates a new element node. 185 * 186 * @param elementBean the container element bean 187 * @param parentContainerNode the parent container node 188 */ 189 public ElementNode(CmsContainerElementBean elementBean, ContainerNode parentContainerNode) { 190 191 m_element = elementBean; 192 m_parentContainerNode = parentContainerNode; 193 } 194 195 /** 196 * Adds the container node as a nested container for the element. 197 * 198 * @param containerNode the container node 199 */ 200 public void add(ContainerNode containerNode) { 201 202 m_containers.put(containerNode.getName(), containerNode); 203 } 204 205 /** 206 * Gets the nested containers for this element as a map, with container names as keys. 207 * 208 * @return the map of nested containers 209 */ 210 public Map<String, ContainerNode> getContainers() { 211 212 return Collections.unmodifiableMap(m_containers); 213 } 214 215 /** 216 * Gets the container element bean which this node is wrapping. 217 * 218 * @return the container element bean 219 */ 220 public CmsContainerElementBean getElement() { 221 222 return m_element; 223 } 224 225 /** 226 * Returns the parent container node. 227 * 228 * @return the parent container node 229 */ 230 public ContainerNode getParentContainerNode() { 231 232 return m_parentContainerNode; 233 } 234 235 } 236 237 /** Logger instance for this class. */ 238 private static final Log LOG = CmsLog.getLog(CmsJsonRendererContainerPage.class); 239 240 /** The CMS context used for VFS operations. */ 241 private CmsObject m_cms; 242 243 /** The container page. */ 244 private CmsResource m_page; 245 246 /** The property filter. */ 247 private Predicate<String> m_propFilter; 248 249 /** 250 * Creates a new renderer instance. 251 * 252 * @param cms the CMS context 253 * @param page the container page to render 254 * @param propertyFilter the property filter 255 */ 256 public CmsJsonRendererContainerPage(CmsObject cms, CmsResource page, Predicate<String> propertyFilter) { 257 258 m_cms = cms; 259 m_page = page; 260 m_propFilter = propertyFilter; 261 } 262 263 /** 264 * Builds a tree from the given container page bean.<p> 265 * 266 * The returned tree consists of container nodes (which have children corresponding the container elements) and element nodes 267 * (which have children corresponding to the nested containers of the element). The root of the tree is a dummy element node 268 * which does not correspond to any element in the page, but just acts as a container for the top-level containers of the page. 269 * 270 * @param page the container page bean 271 * @param rootPath the root path of the container page 272 * @return the dummy root element node 273 */ 274 public ElementNode buildTree(CmsContainerPageBean page, String rootPath) { 275 276 Map<String, ElementNode> elementsByInstanceId = new HashMap<>(); 277 List<ContainerNode> containerNodes = new ArrayList<>(); 278 CmsADEConfigData adeConfig = OpenCms.getADEManager().lookupConfiguration(m_cms, rootPath); 279 280 for (CmsContainerBean container : page.getContainers().values()) { 281 ContainerNode containerNode = new ContainerNode(container); 282 containerNodes.add(containerNode); 283 for (CmsContainerElementBean cachedElementBean : container.getElements()) { 284 CmsContainerElementBean elementBean = cachedElementBean.clone(); 285 try { 286 elementBean.initResource(m_cms); 287 } catch (CmsException e) { 288 // Skip elements whose resources can't be read 289 LOG.warn(e.getLocalizedMessage(), e); 290 continue; 291 } 292 ElementNode elemNode = new ElementNode(elementBean, containerNode); 293 if (elementBean.getInstanceId() != null) { 294 elementsByInstanceId.put(elementBean.getInstanceId(), elemNode); 295 } 296 I_CmsFormatterBean formatter = getFormatter(m_cms, container, elementBean, adeConfig); 297 elementBean.initSettings(m_cms, adeConfig, formatter, Locale.ENGLISH, null, new HashMap<>()); 298 containerNode.add(elemNode); 299 } 300 } 301 302 ElementNode rootElement = new ElementNode(null, null); // Dummy element containing all the top-level containers 303 for (ContainerNode containerNode : containerNodes) { 304 CmsContainerBean container = containerNode.getContainer(); 305 String parentId = container.getParentInstanceId(); 306 ElementNode parentElement = CmsStringUtil.isEmpty(parentId) ? null : elementsByInstanceId.get(parentId); 307 if (parentElement == null) { 308 rootElement.add(containerNode); 309 } else { 310 parentElement.add(containerNode); 311 } 312 313 } 314 return rootElement; 315 316 } 317 318 /** 319 * Renders the JSON for the container page. 320 * 321 * @return the JSON for the container page 322 * @throws Exception if something goes wrong 323 */ 324 public Object renderJson() throws Exception { 325 326 CmsFile file = m_cms.readFile(m_page); 327 CmsXmlContainerPage page = CmsXmlContainerPageFactory.unmarshal(m_cms, file); 328 CmsContainerPageBean pageBean = page.getContainerPage(m_cms); 329 ElementNode root = buildTree(pageBean, file.getRootPath()); 330 return elementToJson(root); 331 } 332 333 /** 334 * Renders a container node as JSON. 335 * 336 * @param containerNode the container node 337 * @return the JSON for the node 338 * @throws JSONException if something goes wrong with JSON processing 339 */ 340 JSONObject containerToJson(ContainerNode containerNode) throws JSONException { 341 342 JSONObject result = new JSONObject(); 343 result.put("name", containerNode.getName()); 344 result.put("type", containerNode.getType()); 345 result.put("isDetailOnlyContainer", containerNode.isDetailOnlyContainer()); 346 result.put("isNestedContainer", containerNode.isNestedContainer()); 347 result.put("isRootContainer", containerNode.isRootContainer()); 348 JSONArray elemJson = new JSONArray(); 349 for (ElementNode elemNode : containerNode.getElements()) { 350 elemJson.put(elementToJson(elemNode)); 351 } 352 result.put("elements", elemJson); 353 return result; 354 } 355 356 /** 357 * Renders an element node as JSON. 358 * 359 * @param elementNode the element node 360 * @return the JSON for the element 361 * @throws JSONException if something goes wrong 362 */ 363 JSONObject elementToJson(ElementNode elementNode) throws JSONException { 364 365 JSONObject result = new JSONObject(); 366 if (elementNode.getElement() != null) { 367 result.put("path", elementNode.getElement().getResource().getRootPath()); 368 // new container page format has property formatterKey 369 String formatterKey = CmsFormatterUtils.getFormatterKey( 370 elementNode.getParentContainerNode().getName(), 371 elementNode.getElement()); 372 result.put("formatterKey", formatterKey); 373 JSONObject settings = new JSONObject(); 374 for (Map.Entry<String, String> entry : elementNode.getElement().getSettings().entrySet()) { 375 // formatterSettings and element_instance_id setting have become obsolete in the new container page format 376 if (entry.getKey().startsWith("formatterSettings") || entry.getKey().equals("element_instance_id")) { 377 continue; 378 } 379 settings.put(entry.getKey(), entry.getValue()); 380 } 381 result.put("settings", settings); 382 } 383 JSONArray containers = new JSONArray(); 384 for (ContainerNode containerNode : elementNode.getContainers().values()) { 385 containers.put(containerToJson(containerNode)); 386 } 387 result.put("containers", containers); 388 return result; 389 } 390 391 /** 392 * Helper method for getting the formatter bean for a container element. 393 * 394 * <p>This only relies on the container element data itself since container width/type are not stored with the container, 395 * so it may return null if no formatter is set as an element setting. 396 * 397 * @param cms the CMS context 398 * @param container the container in which the element is located 399 * @param elementBean the element bean 400 * @param adeConfig the ADE configuration 401 * @return the formatter bean 402 */ 403 private I_CmsFormatterBean getFormatter( 404 CmsObject cms, 405 CmsContainerBean container, 406 CmsContainerElementBean elementBean, 407 CmsADEConfigData adeConfig) { 408 409 Collection<I_CmsFormatterBean> formatterList = adeConfig.getCachedFormatters().getFormattersForType( 410 OpenCms.getResourceManager().getResourceType(elementBean.getResource()).getTypeName(), 411 false); 412 Map<CmsUUID, I_CmsFormatterBean> formatters = new HashMap<>(); 413 for (I_CmsFormatterBean formatter : formatterList) { 414 formatters.put(new CmsUUID(formatter.getId()), formatter); 415 } 416 417 Map<String, String> settings = elementBean.getIndividualSettings(); 418 I_CmsFormatterBean result = null; 419 420 String forKeyWithContainer = settings.get(CmsFormatterConfig.FORMATTER_SETTINGS_KEY + container.getName()); 421 String forKeyWithoutContainer = settings.get(CmsFormatterConfig.FORMATTER_SETTINGS_KEY); 422 for (String formatterId : new String[] {forKeyWithContainer, forKeyWithoutContainer}) { 423 if (CmsUUID.isValidUUID(formatterId)) { 424 result = formatters.get(new CmsUUID(formatterId)); 425 break; 426 } 427 } 428 CmsUUID elementJspId = elementBean.getFormatterId(); 429 if ((result == null) && (elementJspId != null)) { 430 LOG.warn( 431 "Formatter id not found for element " 432 + elementBean.getResource().getRootPath() 433 + " in " 434 + m_page.getRootPath()); 435 436 for (I_CmsFormatterBean bean : formatters.values()) { 437 if (bean.getJspStructureId().equals(elementJspId)) { 438 result = bean; 439 break; 440 } 441 } 442 } 443 return result; 444 445 } 446 447}