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&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}