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}