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 &mdash; 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>"&lt;script src='$S{juneau.datatables.jqueryUrl:https://code.jquery.com/jquery-3.7.1.min.js}'&gt;&lt;/script&gt;"</js>,
072 *          <js>"&lt;script src='$S{juneau.datatables.jsUrl:https://cdn.datatables.net/2.1.8/js/dataTables.min.js}'&gt;&lt;/script&gt;"</js>,
073 *          <js>"&lt;script src='servlet:/juneau-datatables.js'&gt;&lt;/script&gt;"</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} &mdash; see {@link org.apache.juneau.console.datatables.adapter.DataTablesQuery} for
091 * the endpoint side:
092 *
093 * <p class='bhtml'>
094 *    &lt;script src="https://code.jquery.com/jquery-3.7.1.min.js"&gt;&lt;/script&gt;
095 *    &lt;script src="https://cdn.datatables.net/2.1.8/js/dataTables.min.js"&gt;&lt;/script&gt;
096 *    &lt;script src="/releases/juneau-datatables.js"&gt;&lt;/script&gt;
097 *    &lt;table id="releases"&gt;&lt;/table&gt;
098 *    &lt;script&gt;
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 *    &lt;/script&gt;
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] &mdash; 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 &mdash; 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}