001/*
002 * Copyright 2012-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.configurationmetadata;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.nio.charset.Charset;
022import java.util.ArrayList;
023import java.util.List;
024import java.util.Map;
025
026/**
027 * Load a {@link ConfigurationMetadataRepository} from the content of arbitrary
028 * resource(s).
029 *
030 * @author Stephane Nicoll
031 * @since 1.3.0
032 */
033public final class ConfigurationMetadataRepositoryJsonBuilder {
034
035        /**
036         * UTF-8 Charset.
037         */
038        public static final Charset UTF_8 = Charset.forName("UTF-8");
039
040        private Charset defaultCharset = UTF_8;
041
042        private final JsonReader reader = new JsonReader();
043
044        private final List<SimpleConfigurationMetadataRepository> repositories = new ArrayList<SimpleConfigurationMetadataRepository>();
045
046        private ConfigurationMetadataRepositoryJsonBuilder(Charset defaultCharset) {
047                this.defaultCharset = defaultCharset;
048        }
049
050        /**
051         * Add the content of a {@link ConfigurationMetadataRepository} defined by the
052         * specified {@link InputStream} json document using the default charset. If this
053         * metadata repository holds items that were loaded previously, these are ignored.
054         * <p>
055         * Leaves the stream open when done.
056         * @param inputStream the source input stream
057         * @return this builder
058         * @throws IOException in case of I/O errors
059         */
060        public ConfigurationMetadataRepositoryJsonBuilder withJsonResource(
061                        InputStream inputStream) throws IOException {
062                return withJsonResource(inputStream, this.defaultCharset);
063        }
064
065        /**
066         * Add the content of a {@link ConfigurationMetadataRepository} defined by the
067         * specified {@link InputStream} json document using the specified {@link Charset}. If
068         * this metadata repository holds items that were loaded previously, these are
069         * ignored.
070         * <p>
071         * Leaves the stream open when done.
072         * @param inputStream the source input stream
073         * @param charset the charset of the input
074         * @return this builder
075         * @throws IOException in case of I/O errors
076         */
077        public ConfigurationMetadataRepositoryJsonBuilder withJsonResource(
078                        InputStream inputStream, Charset charset) throws IOException {
079                if (inputStream == null) {
080                        throw new IllegalArgumentException("InputStream must not be null.");
081                }
082                this.repositories.add(add(inputStream, charset));
083                return this;
084        }
085
086        /**
087         * Build a {@link ConfigurationMetadataRepository} with the current state of this
088         * builder.
089         * @return this builder
090         */
091        public ConfigurationMetadataRepository build() {
092                SimpleConfigurationMetadataRepository result = new SimpleConfigurationMetadataRepository();
093                for (SimpleConfigurationMetadataRepository repository : this.repositories) {
094                        result.include(repository);
095                }
096                return result;
097        }
098
099        private SimpleConfigurationMetadataRepository add(InputStream in, Charset charset)
100                        throws IOException {
101                try {
102                        RawConfigurationMetadata metadata = this.reader.read(in, charset);
103                        return create(metadata);
104                }
105                catch (Exception ex) {
106                        throw new IllegalStateException("Failed to read configuration metadata", ex);
107                }
108        }
109
110        private SimpleConfigurationMetadataRepository create(
111                        RawConfigurationMetadata metadata) {
112                SimpleConfigurationMetadataRepository repository = new SimpleConfigurationMetadataRepository();
113                repository.add(metadata.getSources());
114                for (ConfigurationMetadataItem item : metadata.getItems()) {
115                        ConfigurationMetadataSource source = getSource(metadata, item);
116                        repository.add(item, source);
117                }
118                Map<String, ConfigurationMetadataProperty> allProperties = repository
119                                .getAllProperties();
120                for (ConfigurationMetadataHint hint : metadata.getHints()) {
121                        ConfigurationMetadataProperty property = allProperties.get(hint.getId());
122                        if (property != null) {
123                                addValueHints(property, hint);
124                        }
125                        else {
126                                String id = hint.resolveId();
127                                property = allProperties.get(id);
128                                if (property != null) {
129                                        if (hint.isMapKeyHints()) {
130                                                addMapHints(property, hint);
131                                        }
132                                        else {
133                                                addValueHints(property, hint);
134                                        }
135                                }
136                        }
137                }
138                return repository;
139        }
140
141        private void addValueHints(ConfigurationMetadataProperty property,
142                        ConfigurationMetadataHint hint) {
143                property.getHints().getValueHints().addAll(hint.getValueHints());
144                property.getHints().getValueProviders().addAll(hint.getValueProviders());
145        }
146
147        private void addMapHints(ConfigurationMetadataProperty property,
148                        ConfigurationMetadataHint hint) {
149                property.getHints().getKeyHints().addAll(hint.getValueHints());
150                property.getHints().getKeyProviders().addAll(hint.getValueProviders());
151        }
152
153        private ConfigurationMetadataSource getSource(RawConfigurationMetadata metadata,
154                        ConfigurationMetadataItem item) {
155                if (item.getSourceType() != null) {
156                        return metadata.getSource(item.getSourceType());
157                }
158                return null;
159        }
160
161        /**
162         * Create a new builder instance using {@link #UTF_8} as the default charset and the
163         * specified json resource.
164         * @param inputStreams the source input streams
165         * @return a new {@link ConfigurationMetadataRepositoryJsonBuilder} instance.
166         * @throws IOException on error
167         */
168        public static ConfigurationMetadataRepositoryJsonBuilder create(
169                        InputStream... inputStreams) throws IOException {
170                ConfigurationMetadataRepositoryJsonBuilder builder = create();
171                for (InputStream inputStream : inputStreams) {
172                        builder = builder.withJsonResource(inputStream);
173                }
174                return builder;
175        }
176
177        /**
178         * Create a new builder instance using {@link #UTF_8} as the default charset.
179         * @return a new {@link ConfigurationMetadataRepositoryJsonBuilder} instance.
180         */
181        public static ConfigurationMetadataRepositoryJsonBuilder create() {
182                return create(UTF_8);
183        }
184
185        /**
186         * Create a new builder instance using the specified default {@link Charset}.
187         * @param defaultCharset the default charset to use
188         * @return a new {@link ConfigurationMetadataRepositoryJsonBuilder} instance.
189         */
190        public static ConfigurationMetadataRepositoryJsonBuilder create(
191                        Charset defaultCharset) {
192                return new ConfigurationMetadataRepositoryJsonBuilder(defaultCharset);
193        }
194
195}