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.rest.server.widget;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023
024import java.io.*;
025
026import org.apache.juneau.commons.io.*;
027import org.apache.juneau.marshall.html.*;
028import org.apache.juneau.rest.server.*;
029
030/**
031 * A subclass of widgets for rendering menu items with drop-down windows.
032 *
033 * <h5 class='section'>See Also:</h5><ul>
034 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlPredefinedWidgets">Predefined Widgets</a>
035 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlWidgets">Widgets</a>
036 * </ul>
037 */
038public abstract class MenuItemWidget extends Widget {
039
040   /**
041    * Optional Javascript to execute immediately after a menu item is shown.
042    *
043    * <p>
044    * Same as {@link #getBeforeShowScript(RestRequest,RestResponse)} except this Javascript gets executed after the popup dialog has become visible.
045    *
046    * @param req The HTTP request object.
047    * @param res The HTTP response object.
048    * @return Javascript code to execute, or <jk>null</jk> if there isn't any.
049    */
050   public String getAfterShowScript(RestRequest req, RestResponse res) {
051      return null;
052   }
053
054   /**
055    * Optional Javascript to execute immediately before a menu item is shown.
056    *
057    * <p>
058    * For example, the following shows how the method could be used to make an AJAX call back to the REST
059    * interface to populate a SELECT element in the contents of the popup dialog:
060    *
061    * <p class='bjava'>
062    *    <ja>@Override</ja>
063    *    <jk>public</jk> String getBeforeShowScript(RestRequest <jv>req</jv>, RestResponse <jv>res</jv>) {
064    *       <jk>return</jk> <js>""</js>
065    *          + <js>"\n   var xhr = new XMLHttpRequest();"</js>
066    *          + <js>"\n   xhr.open('GET', '/petstore/pet?search=status=AVAILABLE&amp;view=id,name', true);"</js>
067    *          + <js>"\n   xhr.setRequestHeader('Accept', 'application/json');"</js>
068    *          + <js>"\n   xhr.onload = function() {"</js>
069    *          + <js>"\n       var pets = JSON.parse(xhr.responseText);"</js>
070    *          + <js>"\n      var select = document.getElementById('addPet_names');"</js>
071    *          + <js>"\n      select.innerHTML = '';"</js>
072    *          + <js>"\n      for (var i in pets) {"</js>
073    *          + <js>"\n         var pet = pets[i];"</js>
074    *          + <js>"\n         var opt = document.createElement('option');"</js>
075    *          + <js>"\n         opt.value = pet.id;"</js>
076    *          + <js>"\n         opt.innerHTML = pet.name;"</js>
077    *          + <js>"\n         select.appendChild(opt);"</js>
078    *          + <js>"\n      }"</js>
079    *          + <js>"\n   }"</js>
080    *          + <js>"\n   xhr.send();"</js>
081    *       ;
082    *    }
083    * </p>
084    *
085    * <p>
086    * Note that it's often easier (and cleaner) to use the {@link #loadScript(RestRequest,String)} method and read the Javascript from
087    * your classpath:
088    *
089    * <p class='bjava'>
090    *    <ja>@Override</ja>
091    *    <jk>public</jk> String getBeforeShowScript(RestRequest <jv>req</jv>, RestResponse <jv>res</jv>) <jk>throws</jk> Exception {
092    *       <jk>return</jk> loadScript(<jv>req</jv>, <js>"AddOrderMenuItem_beforeShow.js"</js>);
093    *    }
094    * </p>
095    *
096    * @param req The HTTP request object.
097    * @param res The HTTP response object.
098    * @return Javascript code to execute, or <jk>null</jk> if there isn't any.
099    */
100   public String getBeforeShowScript(RestRequest req, RestResponse res) {
101      return null;
102   }
103
104   /**
105    * The content of the popup.
106    *
107    * @param req The HTTP request object.
108    * @param res The HTTP response object.
109    * @return
110    *    The content of the popup.
111    *    <br>Can be any of the following types:
112    *    <ul>
113    *       <li>{@link Reader} - Serialized directly to the output.
114    *       <li>{@link CharSequence} - Serialized directly to the output.
115    *       <li>Other - Serialized as HTML using {@link HtmlSerializer#DEFAULT}.
116    *          <br>Note that this includes any of the {@code org.apache.juneau.bean.html5} bean classes.
117    *    </ul>
118    */
119   public abstract Object getContent(RestRequest req, RestResponse res);
120
121   @Override /* Overridden from Widget */
122   @SuppressWarnings({
123      "java:S3776" // Cognitive complexity acceptable for menu item HTML generation
124   })
125   public String getHtml(RestRequest req, RestResponse res) {
126      var sb = new StringBuilder();
127
128      // Need a unique number to define unique function names.
129      Integer id = null;
130
131      var pre = nullIfEmpty(getBeforeShowScript(req, res));
132      var post = nullIfEmpty(getAfterShowScript(req, res));
133
134      sb.append("\n<div class='menu-item'>");
135      if (nn(pre) || nn(post)) {
136         id = getId(req);
137
138         var nonce = req.getAttribute(RestRequest.CSP_NONCE_ATTR).as(String.class).orElse(null);
139         sb.append(nonce != null ? "\n\t<script nonce='" + nonce + "'>" : "\n\t<script>");
140         if (nn(pre)) {
141            sb.append("\n\t\tfunction onPreShow" + id + "() {");
142            sb.append("\n").append(pre);
143            sb.append("\n\t\t}");
144         }
145         if (nn(post)) {
146            sb.append("\n\t\tfunction onPostShow" + id + "() {");
147            sb.append("\n").append(post);
148            sb.append("\n\t\t}");
149         }
150         sb.append("\n\t</script>");
151      }
152      // @formatter:off
153      var onclick = (pre == null ? "" : "onPreShow"+id+"();") + "menuClick(this);" + (post == null ? "" : "onPostShow"+id+"();");
154      sb.append(""
155         + "\n\t<a onclick='"+onclick+"'>"+getLabel(req, res)+"</a>"
156         + "\n<div class='popup-content'>"
157      );
158      // @formatter:on
159      var o = getContent(req, res);
160      if (o instanceof Reader o2) {
161         try (var r = o2; Writer w = new StringBuilderWriter(sb)) {
162            pipe(r, w);
163         } catch (IOException e) {
164            throw toRex(e);
165         }
166      } else if (o instanceof CharSequence o2) {
167         sb.append(o2);
168      } else {
169         // @formatter:off
170         var session = HtmlSerializer.DEFAULT
171            .createSession()
172            .properties(req.getAttributes().asMap())
173            .debug(req.isDebugMarshalling() ? true : null)
174            .uriContext(req.getUriContext())
175            .useWhitespace(req.isPlainText() ? true : null)
176            .resolver(req.getVarResolverSession())
177            .build();
178         // @formatter:on
179         session.indent = 2;
180         try (Writer w = new StringBuilderWriter(sb)) {
181            session.write(o, w);
182         } catch (Exception e) {
183            throw toRex(e);
184         }
185      }
186      // @formatter:off
187      sb.append(""
188         + "\n\t</div>"
189         + "\n</div>"
190      );
191      // @formatter:on
192      return sb.toString();
193   }
194
195   /**
196    * The label for the menu item as it's rendered in the menu bar.
197    *
198    * @param req The HTTP request object.
199    * @param res The HTTP response object.
200    * @return The menu item label.
201    */
202   public abstract String getLabel(RestRequest req, RestResponse res);
203
204   /**
205    * Returns the Javascript needed for the show and hide actions of the menu item.
206    */
207   @Override /* Overridden from Widget */
208   public String getScript(RestRequest req, RestResponse res) {
209      return loadScript(req, "scripts/MenuItemWidget.js");
210   }
211
212   /**
213    * Defines a <js>"menu-item"</js> class that needs to be used on the outer element of the HTML returned by the
214    * {@link #getHtml(RestRequest,RestResponse)} method.
215    */
216   @Override /* Overridden from Widget */
217   public String getStyle(RestRequest req, RestResponse res) {
218      return loadStyle(req, "styles/MenuItemWidget.css");
219   }
220
221   private static Integer getId(RestRequest req) {
222      var id = req.getAttribute("LastMenuItemId").as(Integer.class).orElse(0) + 1;
223      req.setAttribute("LastMenuItemId", id);
224      return id;
225   }
226}