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}