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.microservice.examples;
018
019import java.io.*;
020import java.util.Map;
021
022import org.apache.juneau.commons.*;
023import org.apache.juneau.http.*;
024import org.apache.juneau.http.classic.response.*;
025import org.apache.juneau.marshall.collections.*;
026import org.apache.juneau.marshall.html.*;
027import org.apache.juneau.rest.server.*;
028import org.apache.juneau.rest.server.servlet.*;
029
030/**
031 * Shows contents of the microservice configuration file, and updates it.
032 *
033 * <p>
034 * Reads are {@code GET}s.  Writes are {@code PUT}s only, sent by a REST client or from the command line; there is no
035 * HTML edit form, because a browser form cannot carry the header-borne token a CSRF boundary requires.  A {@code PUT}
036 * cannot be sent cross-site without a CORS preflight, so a boundary in front of this resource (see
037 * {@link AdminBoundary}) admits it without a token:
038 * <p class='bcode'>
039 *    curl -X PUT -H 'Content-Type: text/plain' --data-binary @my.cfg http://localhost:10000/config
040 *    curl -X PUT -H 'Content-Type: text/plain' -d 'newValue' http://localhost:10000/config/MySection/myKey
041 * </p>
042 *
043 * <p>
044 * A {@code GET} returns the whole configuration, including any secrets in it, and this resource has no
045 * authentication of its own.  Do not expose it on an untrusted network.
046 *
047 * <h5 class='section'>See Also:</h5><ul>
048 *    <li class='jc'>{@link AdminBoundary}
049 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroservice">juneau-microservice</a>
050 * </ul>
051 *
052 * @serial exclude
053 */
054@Rest(
055   path="/config",
056   title="Configuration",
057   description="Contents of configuration file."
058)
059@HtmlDocConfig(
060   navlinks={
061      "up: request:/..",
062      "api: servlet:/api",
063      "stats: servlet:/stats"
064   },
065   resolveBodyVars="true"
066)
067@SuppressWarnings({
068   "java:S112" // throws Exception intentional - callback/lifecycle method
069})
070public class ConfigResource extends BasicRestServlet {
071   @Response
072   @Schema(description = "Section not found.")
073   @SuppressWarnings({
074      "java:S110" // Inheritance depth acceptable for microservice hierarchy
075   })
076   private class SectionNotFound extends NotFound {
077      private static final long serialVersionUID = 1L;
078
079      /** Constructor. */
080      SectionNotFound() {
081         super("Section not found.");
082      }
083   }
084
085   private static final long serialVersionUID = 1L;
086
087   @RestGet(
088      path="/",
089      summary="Get config file contents",
090      description="Show contents of config file as a JsonMap.",
091      swagger=@OpSwagger(
092         responses={
093            "200:{ description:'Config file as a map of map of objects.', example:{'':{defaultKey:'defaultValue'},'Section1':{key1:'val1',key2:123}}}"
094         }
095      )
096   )
097   /**
098    * Returns the config file contents as a nested map.
099    *
100    * @return The config as a map of maps.
101    */
102   public JsonMap getConfig() {
103      return getContext().getConfig().toMap();
104   }
105
106   @RestGet(
107      path="/{section}/{key}",
108      summary="Get config file entry value",
109      description="Show value of config file entry as a simple string.",
110      swagger=@OpSwagger(
111         responses={
112            "200:{ description:'Entry value.', example:'servlet:/htdocs/themes/dark.css'}"
113         }
114      )
115   )
116   /**
117    * Returns the value of a config entry for the given section and key.
118    *
119    * @param section Section name in config file.
120    * @param key Key name in section.
121    * @return The entry value.
122    * @throws SectionNotFound If the section is not found.
123    */
124   public String getConfigEntry(
125         @Path("section") @Schema(d="Section name in config file.") String section,
126         @Path("key") @Schema(d="Key name in section.") String key
127      ) throws SectionNotFound {
128
129      return getSection(section).getString(key);
130   }
131
132   @RestGet(
133      path="/{section}",
134      summary="Get config file section contents",
135      description="Show contents of config file section as a JsonMap.",
136      swagger=@OpSwagger(
137         responses={
138            "200:{ description:'Config file section as a map of objects.', example:{key1:'val1',key2:123}}"
139         }
140      )
141   )
142   /**
143    * Returns the contents of a config section as a map.
144    *
145    * @param section Section name in config file.
146    * @return The section as a map.
147    * @throws SectionNotFound If the section is not found.
148    */
149   public JsonMap getConfigSection(
150         @Path("section") @Schema(d="Section name in config file.") String section
151      ) throws SectionNotFound {
152
153      return getSection(section);
154   }
155
156   @Mutating("Changes the configuration")
157   @RestPut(
158      path="/",
159      summary="Update config file contents",
160      description="Update the contents of the config file from raw text.",
161      swagger=@OpSwagger(
162         responses={
163            "200:{ description:'Config file section as a map of objects.', example:{key1:'val1',key2:123}}"
164         }
165      )
166   )
167   /**
168    * Updates the config file from raw text in INI format.
169    *
170    * @param contents New contents in INI file format.
171    * @return The config as a map.
172    * @throws Exception If parsing or loading fails.
173    */
174   public JsonMap setConfigContents(
175         @Content @Schema(d="New contents in INI file format.") Reader contents
176      ) throws Exception {
177
178      return getContext().getConfig().load(contents, true).toMap();
179   }
180
181   @Mutating("Changes the configuration")
182   @RestPut(
183      path="/{section}",
184      summary="Update config section contents",
185      description="Add or overwrite a config file section.",
186      swagger=@OpSwagger(
187         responses={
188            "200:{ description:'Config file section as a map of objects.', example:{key1:'val1',key2:123}}"
189         }
190      )
191   )
192   /**
193    * Adds or overwrites a config section.
194    *
195    * @param section Section name in config file.
196    * @param contents New contents of config section as a simple map of key/value pairs.
197    * @return The updated section as a map.
198    * @throws Exception If the update fails.
199    */
200   public JsonMap setConfigSection(
201         @Path("section") @Schema(d="Section name in config file.") String section,
202         @Content @Schema(d="New contents of config section as a simple map of key/value pairs.")
203         Map<String,Object> contents
204      ) throws Exception {
205
206      getContext().getConfig().setSection(section, null, contents);
207      return getSection(section);
208   }
209
210   @Mutating("Changes the configuration")
211   @RestPut(
212      path="/{section}/{key}",
213      summary="Update config entry value",
214      description="Add or overwrite a config file entry.",
215      swagger=@OpSwagger(
216         responses={
217            "200:{ description:'The updated value.', example:'servlet:/htdocs/themes/dark.css'}"
218         }
219      )
220   )
221   /**
222    * Adds or overwrites a config entry value.
223    *
224    * @param section Section name in config file.
225    * @param key Key name in section.
226    * @param value New value for entry.
227    * @return The updated value.
228    * @throws SectionNotFound If the section is not found.
229    */
230   public String setConfigValue(
231         @Path("section") @Schema(d="Section name in config file.") String section,
232         @Path("key") @Schema(d="Key name in section.") String key,
233         @Content @Schema(d="New value for entry.") String value
234      ) throws SectionNotFound {
235
236      getContext().getConfig().set(section + '/' + key, value);
237      return getSection(section).getString(key);
238   }
239
240   private JsonMap getSection(String name) throws SectionNotFound {
241      return getContext().getConfig().getSection(name).asMap().orElseThrow(SectionNotFound::new);
242   }
243}