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.datatables; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020 021import java.io.*; 022import java.nio.charset.*; 023 024import org.apache.juneau.commons.utils.*; 025import org.apache.juneau.http.*; 026import org.apache.juneau.http.entity.*; 027import org.apache.juneau.http.header.*; 028import org.apache.juneau.http.resource.*; 029import org.apache.juneau.rest.server.*; 030 031/** 032 * Mixin that serves Juneau's first-party DataTables glue script at {@code /juneau-datatables.js}. 033 * 034 * <p> 035 * Compose into a host resource via {@link Rest#mixins() @Rest(mixins=DataTablesMixin.class)}; the 036 * {@code /juneau-datatables.js} URL then becomes available alongside the host's own endpoints, so browser pages can 037 * load the glue without the application hosting it itself. 038 * 039 * <h5 class='section'>Where the DataTables library comes from:</h5> 040 * 041 * <p> 042 * This module declares the jQuery and DataTables WebJars ({@code org.webjars:jquery}, 043 * {@code org.webjars.npm:datatables.net}, {@code datatables.net-dt}, {@code datatables.net-buttons}, 044 * {@code datatables.net-buttons-dt}) as dependencies; Juneau's own jars don't copy their files. 045 * {@link org.apache.juneau.rest.server.staticfile.WebJarsMixin} serves them from the classpath. This mixin serves 046 * only the first-party {@code juneau-datatables.js} glue, which auto-initializes any server-rendered 047 * {@code <table data-juneau-datatable>} (e.g. one built by {@link DataTablesTable}) by calling 048 * {@code $(table).DataTable(opts)} once the library is present. 049 * 050 * <p> 051 * On a console page ({@code juneau-console-freemarker}), every {@code <@card type="datatables">} 052 * requires the {@code "datatables-glue"} toolkit pack, which pulls jQuery and DataTables in before the glue. Compose 053 * this mixin and {@code WebJarsMixin} into the host; the page never loads the library by hand. Outside a console 054 * page, wire the assets yourself as below. 055 * 056 * <h5 class='section'>Wiring the page assets (caller-side {@code @HtmlDocConfig}):</h5> 057 * 058 * <p> 059 * Juneau's {@link org.apache.juneau.marshall.html.HtmlDocConfig @HtmlDocConfig} is resolved per endpoint / per 060 * resource-class hierarchy — it is <b>not</b> contributed across a host's other endpoints by a mixin. So a mixin 061 * cannot silently inject page assets into <i>your</i> data endpoints; instead, add the wire-points to the resource (or 062 * op) that renders the HTML table, referencing your chosen DataTables distribution and this served glue. The URLs are 063 * SVL-resolvable, so the DataTables location can be overridden per environment without code changes. 064 * 065 * <h5 class='section'>Example:</h5> 066 * <p class='bjava'> 067 * <ja>@Rest</ja>(path=<js>"/releases"</js>, mixins=DataTablesMixin.<jk>class</jk>) 068 * <ja>@HtmlDocConfig</ja>( 069 * stylesheet=<js>"$S{juneau.datatables.cssUrl:https://cdn.datatables.net/2.1.8/css/dataTables.dataTables.min.css}"</js>, 070 * head={ 071 * <js>"<script src='$S{juneau.datatables.jqueryUrl:https://code.jquery.com/jquery-3.7.1.min.js}'></script>"</js>, 072 * <js>"<script src='$S{juneau.datatables.jsUrl:https://cdn.datatables.net/2.1.8/js/dataTables.min.js}'></script>"</js>, 073 * <js>"<script src='servlet:/juneau-datatables.js'></script>"</js> 074 * } 075 * ) 076 * <jk>public class</jk> ReleasesResource <jk>extends</jk> BasicRestServlet { 077 * <ja>@RestGet</ja> 078 * <jk>public</jk> Table getReleases() { 079 * <jk>return</jk> DataTablesTable.<jsm>of</jsm>(<js>"releases"</js>, <jv>releases</jv>, Release.<jk>class</jk>); 080 * } 081 * } 082 * </p> 083 * 084 * <h5 class='section'>Server-side processing ({@code JuneauDataTables.ajax}):</h5> 085 * 086 * <p> 087 * For a large table, let DataTables fetch pages from the server instead of rendering every row. The served glue 088 * exposes <c>JuneauDataTables.ajax(url, extra?)</c>, which returns a DataTables {@code ajax} option that POSTs the 089 * server-side request as JSON to a {@code @RestOp(method="POST")} endpoint taking {@code @Content} 090 * {@link DataTablesRequest} — see {@link org.apache.juneau.console.datatables.adapter.DataTablesQuery} for 091 * the endpoint side: 092 * 093 * <p class='bhtml'> 094 * <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> 095 * <script src="https://cdn.datatables.net/2.1.8/js/dataTables.min.js"></script> 096 * <script src="/releases/juneau-datatables.js"></script> 097 * <table id="releases"></table> 098 * <script> 099 * new DataTable('#releases', { 100 * serverSide: true, 101 * ajax: JuneauDataTables.ajax('/releases/query'), 102 * columns: [{data: 'name', title: 'Name'}, {data: 'version', title: 'Version'}] 103 * }); 104 * </script> 105 * </p> 106 * 107 * <p> 108 * A server-rendered {@link DataTablesTable} can skip the inline {@code <script>} entirely: give it the 109 * {@link DataTablesTable#AJAX_ATTR} attribute (e.g. via a direct {@code Table} DOM edit after 110 * {@link DataTablesTable#of(String, java.util.Collection, Class) DataTablesTable.of}) and the glue wires 111 * {@code opts.ajax}/{@code opts.serverSide} automatically on load. 112 * 113 * <h5 class='section'>Mixin-only deployment:</h5> 114 * 115 * <p> 116 * This resource is designed for composition via {@code @Rest(mixins=...)}. The mount path is pinned at the op level by 117 * {@link RestGet @RestGet(path="/juneau-datatables.js")} on {@link #getGlueScript}; a class-level 118 * {@code @Rest(paths=...)} declaration would be silently ignored under the mixin pattern. {@code WebJarsMixin} follows the same 119 * pattern for {@code /webjars/*}. 120 * 121 * <h5 class='section'>See Also:</h5><ul> 122 * <li class='jc'>{@link DataTablesTable} 123 * <li class='jc'>{@link DataTablesColumns} 124 * <li class='jc'>{@link org.apache.juneau.console.datatables.adapter.DataTablesQuery} 125 * <li class='link'><a class="doclink" href="https://datatables.net/examples/data_sources/dom">DataTables from a pre-rendered DOM table</a> 126 * </ul> 127 * 128 * @since 10.0.0 129 */ 130// @formatter:off 131@Rest 132public class DataTablesMixin { 133 134 /** The URL path at which the first-party glue script is served (relative to the host mount). */ 135 public static final String GLUE_PATH = "/juneau-datatables.js"; 136 137 /** Classpath location of the shipped glue script (resolved against this class's package). */ 138 static final String GLUE_RESOURCE = "juneau-datatables.js"; 139 140 /** Content type emitted for the glue script. */ 141 static final String GLUE_CONTENT_TYPE = "text/javascript;charset=utf-8"; 142 143 /** {@code Cache-Control} header emitted for the glue script (1 day). */ 144 static final String GLUE_CACHE_CONTROL = "max-age=86400, public"; 145 146 /** The shipped glue script bytes, read once from the classpath on first request. */ 147 @SuppressWarnings({ 148 "java:S3077" // Double-checked-locking safe publication of one whole immutable array reference, not per-element mutation; AtomicReferenceArray solves a different problem. 149 }) 150 private static volatile byte[] glueScript; 151 152 /** 153 * [GET /juneau-datatables.js] — serve the first-party DataTables glue script. 154 * 155 * @return The glue script as a JavaScript {@link HttpResource}. 156 * @throws IOException If the shipped glue resource could not be read (effectively unreachable — the resource 157 * is shipped in the same jar as this class). 158 */ 159 @RestGet( 160 path=GLUE_PATH, 161 summary="Juneau DataTables glue script", 162 description="First-party JavaScript that auto-initializes server-rendered <table data-juneau-datatable> elements.", 163 swagger=@OpSwagger(ignore=true) 164 ) 165 public HttpResource getGlueScript() throws IOException { 166 return HttpResourceBean.of( 167 ByteArrayBody.of(glueScript(), GLUE_CONTENT_TYPE), 168 list(ContentType.of(GLUE_CONTENT_TYPE), CacheControl.of(GLUE_CACHE_CONTROL)) 169 ); 170 } 171 172 /** Returns the shipped glue-script bytes, reading (and caching) them from the classpath on first call. */ 173 @SuppressWarnings({ 174 "resource" // IoUtils.read(InputStream) closes the stream; JDT can't see through the call. 175 }) 176 private static byte[] glueScript() throws IOException { 177 var g = glueScript; 178 if (g == null) { 179 synchronized (DataTablesMixin.class) { 180 g = glueScript; 181 if (g == null) { // HTT: the "already set" branch is only reachable if another thread wins the race between the two reads - unhittable single-threaded. 182 g = IoUtils.read(DataTablesMixin.class.getResourceAsStream(GLUE_RESOURCE)).getBytes(StandardCharsets.UTF_8); 183 glueScript = g; 184 } 185 } 186 } 187 return g; 188 } 189}