001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.console.views; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022 023import org.apache.juneau.marshall.collections.*; 024 025/** 026 * Row-detail panel for a table: the per-row GET endpoint, title template, icon and the row-detail region. 027 * 028 * <p> 029 * {@link #CONTRACT_VERSION} and {@link #response(Object)} describe the JSON that endpoint returns. 030 * 031 * <h5 class='section'>Example:</h5> 032 * <p class='bjava'> 033 * <jv>table</jv>.detail(RowDetail.<jsm>create</jsm>(<js>"/rest/work/data/{id}"</js>) 034 * .title(<js>"{workId}"</js>).icon(<js>"search"</js>) 035 * .region(RegionDef.<jsm>create</jsm>(<js>"work-detail"</js>).populate(<js>"work-detail"</js>) 036 * .allowPopulators(<js>"work-detail"</js>))); 037 * 038 * <jc>// The matching endpoint:</jc> 039 * <ja>@RestGet</ja>(path=<js>"/data/{id}"</js>) 040 * <jk>public</jk> JsonMap detail(<ja>@Path</ja> String <jv>id</jv>) { 041 * <jk>return</jk> RowDetail.<jsm>response</jsm>(<jv>store</jv>.fields(<jv>id</jv>)); 042 * } 043 * </p> 044 * 045 * @since 10.0.0 046 */ 047public final class RowDetail { 048 049 /** The contract version for the detail GET response (the card fragment itself carries no version). */ 050 public static final String CONTRACT_VERSION = "1"; 051 052 private final String endpoint; 053 private String title; 054 private String icon; 055 private RegionDef region; 056 057 private RowDetail(String endpoint) { 058 this.endpoint = endpoint; 059 } 060 061 /** 062 * Starts a row-detail definition for the given per-row GET endpoint. 063 * 064 * @param endpoint A same-origin path template carrying the literal {@code {id}} row-id placeholder (the client substitutes only that), e.g. {@code "/rest/work/data/{id}"}. 065 * Must not be <jk>null</jk> or blank. 066 * @return A new {@link RowDetail}. 067 * @throws IllegalArgumentException If {@code endpoint} is not a same-origin path with the literal {@code {id}} placeholder. 068 */ 069 public static RowDetail create(String endpoint) { 070 if (! RegionDef.isSafeDetailEndpoint(endpoint) || ! endpoint.contains("{id}")) 071 throw iaex("RowDetail endpoint '%s' is not a safe detail endpoint (same-origin path with an {id} placeholder).", endpoint); 072 return new RowDetail(endpoint); 073 } 074 075 /** 076 * Sets the detail panel's title template. 077 * 078 * @param template A row-data interpolation template, e.g. {@code "{workId}"}. Can be <jk>null</jk> to unset. 079 * @return This object. 080 */ 081 public RowDetail title(String template) { 082 title = template; 083 return this; 084 } 085 086 /** 087 * Sets the detail panel's icon. 088 * 089 * @param name A glyph name resolved by the shared icon registry. Can be <jk>null</jk> to unset. 090 * @return This object. 091 */ 092 public RowDetail icon(String name) { 093 icon = name; 094 return this; 095 } 096 097 /** 098 * Sets the detail panel's region. 099 * 100 * @param value The region. Can be <jk>null</jk> to clear it (a header-only panel). 101 * Its {@link RegionDef#type} defaults to {@link RegionDef#TYPE_ROW_DETAIL} and its 102 * {@link RegionDef#dataUrl} defaults to this detail's own endpoint, applied at {@link #toMap()} time onto a 103 * copy; this method never mutates {@code value} itself. 104 * @return This object. 105 */ 106 public RowDetail region(RegionDef value) { 107 region = value; 108 return this; 109 } 110 111 /** 112 * Builds the detail GET endpoint's own JSON response (not the card fragment). 113 * 114 * @param fields The row's detail fields, a map (or bean) in whatever shape the detail region's populator/renderer expects. 115 * @return {@code {contractVersion, fields}}. 116 */ 117 public static JsonMap response(Object fields) { 118 var m = new JsonMap(); 119 m.put("contractVersion", CONTRACT_VERSION); 120 m.put("fields", fields); 121 return m; 122 } 123 124 /** 125 * Builds this detail's card-body entry: {@code {endpoint, title?, icon?, region?}}. 126 * 127 * <p> 128 * The region is optional: a detail with none is a header-only panel (the client fills the {@code title}/{@code icon} 129 * header from the endpoint's {@code fields}) and the entry carries no {@code region} key. 130 * 131 * <p> 132 * When a region is set, builds from a copy: reads {@link RegionDef#toContractMap()} off the caller's {@link #region(RegionDef) region} 133 * into a fresh map, applies the {@code type}/{@code dataUrl} defaults onto that copy, and strips the copy's 134 * {@code contractVersion} key (a card fragment never nests a {@code contractVersion}, since the client stamps it 135 * on mount). The caller's {@link RegionDef} instance is never written to. 136 * 137 * @return The detail entry. 138 */ 139 public JsonMap toMap() { 140 var m = new JsonMap(); 141 m.put("endpoint", endpoint); 142 if (title != null) 143 m.put("title", title); 144 if (icon != null) 145 m.put("icon", icon); 146 if (region != null) { 147 var regionMap = new LinkedHashMap<>(region.toContractMap()); 148 regionMap.remove("contractVersion"); 149 regionMap.putIfAbsent("type", RegionDef.TYPE_ROW_DETAIL); 150 regionMap.putIfAbsent("dataUrl", endpoint); 151 m.put("region", regionMap); 152 } 153 return m; 154 } 155}