From ff23aca6981b68eb1d6c1ccbe3cb1f51c69b6b2f Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 6 Oct 2016 15:26:04 -0400 Subject: [PATCH] Boot properties metadata excluding JDT dependencies --- .../.gitignore | 2 + .../.settings/org.eclipse.jdt.core.prefs | 5 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../application-properties-metadata/pom.xml | 51 + .../ConfigurationMetadataGroup.java | 76 + .../ConfigurationMetadataHint.java | 74 + .../ConfigurationMetadataItem.java | 60 + .../ConfigurationMetadataProperty.java | 190 ++ .../ConfigurationMetadataRepository.java | 47 + ...gurationMetadataRepositoryJsonBuilder.java | 231 +++ .../ConfigurationMetadataSource.java | 131 ++ .../configurationmetadata/Deprecation.java | 66 + .../DescriptionExtractor.java | 58 + .../boot/configurationmetadata/Hints.java | 82 + .../configurationmetadata/JsonReader.java | 195 ++ .../boot/configurationmetadata/README.txt | 10 + .../RawConfigurationMetadata.java | 106 ++ ...SimpleConfigurationMetadataRepository.java | 130 ++ .../boot/configurationmetadata/ValueHint.java | 97 + .../configurationmetadata/ValueProvider.java | 66 + .../configurationmetadata/package-info.java | 20 + .../ide/eclipse/org/json/JSONObject.java | 1663 +++++++++++++++++ .../properties/metadata/PropertiesLoader.java | 164 ++ .../properties/metadata/PropertyInfo.java | 217 +++ .../SpringPropertiesIndexManager.java | 59 + .../metadata/SpringPropertyIndex.java | 128 ++ .../metadata/ValueProviderRegistry.java | 93 + .../boot/properties/util/ArrayUtils.java | 48 + .../boot/properties/util/CollectionUtil.java | 24 + .../vscode/boot/properties/util/FuzzyMap.java | 169 ++ .../vscode/boot/properties/util/Listener.java | 20 + .../boot/properties/util/ListenerManager.java | 36 + .../boot/properties/util/StringUtil.java | 132 ++ .../ide/vscode/util/ListenerList.java | 250 +++ 34 files changed, 4704 insertions(+) create mode 100644 vscode-extensions/commons/application-properties-metadata/.gitignore create mode 100644 vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.jdt.core.prefs create mode 100644 vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.m2e.core.prefs create mode 100644 vscode-extensions/commons/application-properties-metadata/pom.xml create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Deprecation.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/DescriptionExtractor.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Hints.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/README.txt create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/eclipse/org/json/JSONObject.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesLoader.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertyInfo.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertiesIndexManager.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertyIndex.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/ValueProviderRegistry.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ArrayUtils.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/CollectionUtil.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/FuzzyMap.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/Listener.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ListenerManager.java create mode 100644 vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/StringUtil.java create mode 100644 vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ListenerList.java diff --git a/vscode-extensions/commons/application-properties-metadata/.gitignore b/vscode-extensions/commons/application-properties-metadata/.gitignore new file mode 100644 index 000000000..88b8d0751 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/.gitignore @@ -0,0 +1,2 @@ +/.classpath +/.project diff --git a/vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.jdt.core.prefs b/vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..714351aec --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.m2e.core.prefs b/vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..f897a7f1c --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/vscode-extensions/commons/application-properties-metadata/pom.xml b/vscode-extensions/commons/application-properties-metadata/pom.xml new file mode 100644 index 000000000..6a7762a82 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + + org.springframework.ide.vscode + commons-parent + 0.0.1-SNAPSHOT + ../pom.xml + + application-properties-metadata + Builds metadata for boot application properties based on project's classpath contents + + + + + com.google.guava + guava + 18.0 + + + + javax.inject + javax.inject + 1 + + + + + + + org.springframework.ide.vscode + util-commons + ${project.version} + + + + + org.eclipse.jdt + core + 3.3.0-v_771 + + + + + \ No newline at end of file diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java new file mode 100644 index 000000000..ea6428e85 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * Gather a collection of {@link ConfigurationMetadataProperty properties} that are + * sharing a {@link #getId() common prefix}. Provide access to all the + * {@link ConfigurationMetadataSource sources} that have contributed properties to the + * group. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class ConfigurationMetadataGroup implements Serializable { + + private final String id; + + private final Map sources = new HashMap(); + + private final Map properties = new HashMap(); + + public ConfigurationMetadataGroup(String id) { + this.id = id; + } + + /** + * Return the id of the group, used as a common prefix for all properties associated + * to it. + * @return the id of the group + */ + public String getId() { + return this.id; + } + + /** + * Return the {@link ConfigurationMetadataSource sources} defining the properties of + * this group. + * @return the sources of the group + */ + public Map getSources() { + return this.sources; + } + + /** + * Return the {@link ConfigurationMetadataProperty properties} defined in this group. + *

+ * A property may appear more than once for a given source, potentially with + * conflicting type or documentation. This is a "merged" view of the properties of + * this group. + * @return the properties of the group + * @see ConfigurationMetadataSource#getProperties() + */ + public Map getProperties() { + return this.properties; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java new file mode 100644 index 000000000..9f5e1f204 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * A raw view of a hint used for parsing only. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class ConfigurationMetadataHint { + + private static final String KEY_SUFFIX = ".keys"; + + private static final String VALUE_SUFFIX = ".values"; + + private String id; + + private final List valueHints = new ArrayList(); + + private final List valueProviders = new ArrayList(); + + public boolean isMapKeyHints() { + return (this.id != null && this.id.endsWith(KEY_SUFFIX)); + } + + public boolean isMapValueHints() { + return (this.id != null && this.id.endsWith(VALUE_SUFFIX)); + } + + public String resolveId() { + if (isMapKeyHints()) { + return this.id.substring(0, this.id.length() - KEY_SUFFIX.length()); + } + if (isMapValueHints()) { + return this.id.substring(0, this.id.length() - VALUE_SUFFIX.length()); + } + return this.id; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public List getValueHints() { + return this.valueHints; + } + + public List getValueProviders() { + return this.valueProviders; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java new file mode 100644 index 000000000..4001c4da1 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +/** + * An extension of {@link ConfigurationMetadataProperty} that provides a reference to its + * source. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class ConfigurationMetadataItem extends ConfigurationMetadataProperty { + + private String sourceType; + + private String sourceMethod; + + /** + * The class name of the source that contributed this property. For example, if the + * property was from a class annotated with {@code @ConfigurationProperties} this + * attribute would contain the fully qualified name of that class. + * @return the source type + */ + public String getSourceType() { + return this.sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + /** + * The full name of the method (including parenthesis and argument types) that + * contributed this property. For example, the name of a getter in a + * {@code @ConfigurationProperties} annotated class. + * @return the source method + */ + public String getSourceMethod() { + return this.sourceMethod; + } + + public void setSourceMethod(String sourceMethod) { + this.sourceMethod = sourceMethod; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java new file mode 100644 index 000000000..997746490 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; +import java.util.List; + +/** + * Define a configuration property. Each property is fully identified by its + * {@link #getId() id} which is composed of a namespace prefix (the + * {@link ConfigurationMetadataGroup#getId() group id}), if any and the {@link #getName() + * name} of the property. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class ConfigurationMetadataProperty implements Serializable { + + private String id; + + private String name; + + private String type; + + private String description; + + private String shortDescription; + + private Object defaultValue; + + private final Hints hints = new Hints(); + + private Deprecation deprecation; + + /** + * The full identifier of the property, in lowercase dashed form (e.g. + * my.group.simple-property) + * @return the property id + */ + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * The name of the property, in lowercase dashed form (e.g. simple-property). If this + * item does not belong to any group, the id is returned. + * @return the property name + */ + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The class name of the data type of the property. For example, + * {@code java.lang.String}. + *

+ * For consistency, the type of a primitive is specified using its wrapper + * counterpart, i.e. {@code boolean} becomes {@code java.lang.Boolean}. If the type + * holds generic information, these are provided as well, i.e. a {@code HashMap} of + * String to Integer would be defined as {@code java.util.HashMap + * }. + *

+ * Note that this class may be a complex type that gets converted from a String as + * values are bound. + * @return the property type + */ + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + /** + * A description of the property, if any. Can be multi-lines. + * @return the property description + * @see #getShortDescription() + */ + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * A single-line, single-sentence description of this property, if any. + * @return the property short description + * @see #getDescription() + */ + public String getShortDescription() { + return this.shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + /** + * The default value, if any. + * @return the default value + */ + public Object getDefaultValue() { + return this.defaultValue; + } + + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Return the hints of this item. + * @return the hints + */ + public Hints getHints() { + return this.hints; + } + + /** + * The list of well-defined values, if any. If no extra {@link ValueProvider provider} + * is specified, these values are to be considered a closed-set of the available + * values for this item. + * @return the value hints + * @see #getHints() + */ + @Deprecated + public List getValueHints() { + return this.hints.getValueHints(); + } + + /** + * The value providers that are applicable to this item. Only one + * {@link ValueProvider} is enabled for an item: the first in the list that is + * supported should be used. + * @return the value providers + * @see #getHints() + */ + @Deprecated + public List getValueProviders() { + return this.hints.getValueProviders(); + } + + /** + * The {@link Deprecation} for this property, if any. + * @return the deprecation + * @see #isDeprecated() + */ + public Deprecation getDeprecation() { + return this.deprecation; + } + + public void setDeprecation(Deprecation deprecation) { + this.deprecation = deprecation; + } + + /** + * Specify if the property is deprecated. + * @return if the property is deprecated + * @see #getDeprecation() + */ + public boolean isDeprecated() { + return this.deprecation != null; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java new file mode 100644 index 000000000..95122ac2a --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.util.Map; + +/** + * A repository of configuration metadata. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public interface ConfigurationMetadataRepository { + + /** + * Defines the name of the "root" group, that is the group that gathers all the + * properties that aren't attached to a specific group. + */ + String ROOT_GROUP = "_ROOT_GROUP_"; + + /** + * Return the groups, indexed by id. + * @return all configuration meta-data groups + */ + Map getAllGroups(); + + /** + * Return the properties, indexed by id. + * @return all configuration meta-data properties + */ + Map getAllProperties(); + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java new file mode 100644 index 000000000..b7d096fd8 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java @@ -0,0 +1,231 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.ide.eclipse.org.json.JSONException; + +/** + * Load a {@link ConfigurationMetadataRepository} from the content of arbitrary + * resource(s). + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public final class ConfigurationMetadataRepositoryJsonBuilder { + + /** + * UTF-8 Charset. + */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Charset defaultCharset = UTF_8; + + private final JsonReader reader = new JsonReader(); + + private final List rawDatas = new ArrayList<>(); + + private ConfigurationMetadataRepositoryJsonBuilder(Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Add the content of a {@link ConfigurationMetadataRepository} defined by the + * specified {@link InputStream} json document using the default charset. If this + * metadata repository holds items that were loaded previously, these are ignored. + *

+ * Leaves the stream open when done. + * @param origin optional information object to help identify where the inputstream came from + * @param inputStream the source input stream + * @return this builder + * @throws IOException in case of I/O errors + */ + public ConfigurationMetadataRepositoryJsonBuilder withJsonResource( + Object origin, InputStream inputStream) throws IOException { + return withJsonResource(origin, inputStream, this.defaultCharset); + } + + /** + * Add the content of a {@link ConfigurationMetadataRepository} defined by the + * specified {@link InputStream} json document using the specified {@link Charset}. If + * this metadata repository holds items that were loaded previously, these are + * ignored. + *

+ * Leaves the stream open when done. + * @param origin optional information object to help identify where the inputstream came from + * @param inputStream the source input stream + * @param charset the charset of the input + * @return this builder + * @throws IOException in case of I/O errors + */ + public ConfigurationMetadataRepositoryJsonBuilder withJsonResource( + Object origin, InputStream inputStream, Charset charset) throws IOException { + if (inputStream == null) { + throw new IllegalArgumentException("InputStream must not be null."); + } + this.rawDatas.add(parseRaw(origin, inputStream, charset)); + return this; + } + + /** + * Build a {@link ConfigurationMetadataRepository} with the current state of this + * builder. + * @return this builder + */ + public ConfigurationMetadataRepository build() { + SimpleConfigurationMetadataRepository result = new SimpleConfigurationMetadataRepository(); + result.include(create(rawDatas)); + return result; + } + + private RawConfigurationMetadata parseRaw(Object origin, InputStream in, Charset charset) + throws IOException { + try { + return this.reader.read(origin, in, charset); + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Failed to read configuration " + "metadata", ex); + } + catch (JSONException ex) { + throw new IllegalArgumentException( + "Invalid configuration " + "metadata document", ex); + } + } + + private SimpleConfigurationMetadataRepository create( + Iterable metadatas) { + SimpleConfigurationMetadataRepository repository = new SimpleConfigurationMetadataRepository(); + + for (RawConfigurationMetadata metadata : metadatas) { + repository.add(metadata.getSources()); + } + for (RawConfigurationMetadata metadata : metadatas) { + for (ConfigurationMetadataItem item : metadata.getItems()) { + ConfigurationMetadataSource source = getSource(metadata, item); + repository.add(item, source); + } + } + for (RawConfigurationMetadata metadata : metadatas) { + Map allProperties = repository + .getAllProperties(); + for (ConfigurationMetadataHint hint : metadata.getHints()) { + ConfigurationMetadataProperty property = allProperties.get(hint.getId()); + if (property != null) { + addValueHints(property, hint); + } + else { + String id = hint.resolveId(); + property = allProperties.get(id); + if (property != null) { + if (hint.isMapKeyHints()) { + addMapHints(property, hint); + } + else { + addValueHints(property, hint); + } + } + } + } + } + return repository; + } + + private void addValueHints(ConfigurationMetadataProperty property, + ConfigurationMetadataHint hint) { + addAll(property.getHints().getValueHints(), hint.getValueHints()); + property.getHints().getValueProviders().addAll(hint.getValueProviders()); + } + + private void addMapHints(ConfigurationMetadataProperty property, + ConfigurationMetadataHint hint) { + addAll(property.getHints().getKeyHints(), hint.getValueHints()); + property.getHints().getKeyProviders().addAll(hint.getValueProviders()); + } + + /** + * Add a bunch of hints to a list, but guard against duplicates. + */ + private void addAll(List existing, List toAdd) { + if (existing.isEmpty()) { + existing.addAll(toAdd); + } else if (toAdd.isEmpty()) { + //nothing to add + } else { + Set existingValues = existing + .stream() + .map((hint) -> ""+hint.getValue()) + .collect(Collectors.toSet()); + for (ValueHint hint : toAdd) { + if (!existingValues.contains(""+hint.getValue())) { + existing.add(hint); + } + } + } + } + + private ConfigurationMetadataSource getSource(RawConfigurationMetadata metadata, + ConfigurationMetadataItem item) { + if (item.getSourceType() != null) { + return metadata.getSource(item.getSourceType()); + } + return null; + } + + /** + * Create a new builder instance using {@link #UTF_8} as the default charset and the + * specified json resource. + * @param inputStreams the source input streams + * @return a new {@link ConfigurationMetadataRepositoryJsonBuilder} instance. + * @throws IOException on error + */ + public static ConfigurationMetadataRepositoryJsonBuilder create( + InputStream... inputStreams) throws IOException { + ConfigurationMetadataRepositoryJsonBuilder builder = create(); + for (InputStream inputStream : inputStreams) { + builder = builder.withJsonResource(null, inputStream); + } + return builder; + } + + /** + * Create a new builder instance using {@link #UTF_8} as the default charset. + * @return a new {@link ConfigurationMetadataRepositoryJsonBuilder} instance. + */ + public static ConfigurationMetadataRepositoryJsonBuilder create() { + return create(UTF_8); + } + + /** + * Create a new builder instance using the specified default {@link Charset}. + * @param defaultCharset the default charset to use + * @return a new {@link ConfigurationMetadataRepositoryJsonBuilder} instance. + */ + public static ConfigurationMetadataRepositoryJsonBuilder create( + Charset defaultCharset) { + return new ConfigurationMetadataRepositoryJsonBuilder(defaultCharset); + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java new file mode 100644 index 000000000..9c1dad953 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * A source of configuration metadata. Also defines where the source is declared, for + * instance if it is defined as a {@code @Bean}. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class ConfigurationMetadataSource implements Serializable { + + private String groupId; + + private String type; + + private String description; + + private String shortDescription; + + private String sourceType; + + private String sourceMethod; + + private final Map properties = new HashMap(); + + /** + * The identifier of the group to which this source is associated. + * @return the group id + */ + public String getGroupId() { + return this.groupId; + } + + void setGroupId(String groupId) { + this.groupId = groupId; + } + + /** + * The type of the source. Usually this is the fully qualified name of a class that + * defines configuration items. This class may or may not be available at runtime. + * @return the type + */ + public String getType() { + return this.type; + } + + void setType(String type) { + this.type = type; + } + + /** + * A description of this source, if any. Can be multi-lines. + * @return the description + * @see #getShortDescription() + */ + public String getDescription() { + return this.description; + } + + void setDescription(String description) { + this.description = description; + } + + /** + * A single-line, single-sentence description of this source, if any. + * @return the short description + * @see #getDescription() + */ + public String getShortDescription() { + return this.shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + /** + * The type where this source is defined. This can be identical to the + * {@link #getType() type} if the source is self-defined. + * @return the source type + */ + public String getSourceType() { + return this.sourceType; + } + + void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + /** + * The method name that defines this source, if any. + * @return the source method + */ + public String getSourceMethod() { + return this.sourceMethod; + } + + void setSourceMethod(String sourceMethod) { + this.sourceMethod = sourceMethod; + } + + /** + * Return the properties defined by this source. + * @return the properties + */ + public Map getProperties() { + return this.properties; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Deprecation.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Deprecation.java new file mode 100644 index 000000000..8261a2a85 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Deprecation.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; + +/** + * Indicate that a property is deprecated. Provide additional information about the + * deprecation. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class Deprecation implements Serializable { + + private String reason; + + private String replacement; + + /** + * A reason why the related property is deprecated, if any. Can be multi-lines. + * @return the deprecation reason + */ + public String getReason() { + return this.reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + /** + * The full name of the property that replaces the related deprecated property, if + * any. + * @return the replacement property name + */ + public String getReplacement() { + return this.replacement; + } + + public void setReplacement(String replacement) { + this.replacement = replacement; + } + + @Override + public String toString() { + return "Deprecation{" + "reason='" + this.reason + '\'' + ", replacement='" + + this.replacement + '\'' + '}'; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/DescriptionExtractor.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/DescriptionExtractor.java new file mode 100644 index 000000000..81a9ff9b4 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/DescriptionExtractor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.text.BreakIterator; +import java.util.Locale; + +/** + * Utility to extract a description. + * + * @author Stephane Nicoll + */ +class DescriptionExtractor { + + private static final String NEW_LINE = System.getProperty("line.separator"); + + public String getShortDescription(String description) { + if (description == null) { + return null; + } + int dot = description.indexOf("."); + if (dot != -1) { + BreakIterator breakIterator = BreakIterator.getSentenceInstance(Locale.US); + breakIterator.setText(description); + String text = description + .substring(breakIterator.first(), breakIterator.next()).trim(); + return removeSpaceBetweenLine(text); + } + else { + String[] lines = description.split(NEW_LINE); + return lines[0].trim(); + } + } + + private String removeSpaceBetweenLine(String text) { + String[] lines = text.split(NEW_LINE); + StringBuilder sb = new StringBuilder(); + for (String line : lines) { + sb.append(line.trim()).append(" "); + } + return sb.toString().trim(); + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Hints.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Hints.java new file mode 100644 index 000000000..26bdcb69d --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/Hints.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hints of an item to provide the list of values and/or the name of the provider + * responsible to identify suitable values. If the type of the related item is a + * {@link java.util.Map} it can have both key and value hints. + * + * @author Stephane Nicoll + * @since 1.4.0 + */ +public class Hints { + + private final List keyHints = new ArrayList(); + + private final List keyProviders = new ArrayList(); + + private final List valueHints = new ArrayList(); + + private final List valueProviders = new ArrayList(); + + /** + * The list of well-defined keys, if any. Only applicable if the type of the related + * item is a {@link java.util.Map}. If no extra {@link ValueProvider provider} is + * specified, these values are to be considered a closed-set of the available keys for + * the map. + * @return the key hints + */ + public List getKeyHints() { + return this.keyHints; + } + + /** + * The value providers that are applicable to the keys of this item. Only applicable + * if the type of the related item is a {@link java.util.Map}. Only one + * {@link ValueProvider} is enabled for a key: the first in the list that is supported + * should be used. + * @return the key providers + */ + public List getKeyProviders() { + return this.keyProviders; + } + + /** + * The list of well-defined values, if any. If no extra {@link ValueProvider provider} + * is specified, these values are to be considered a closed-set of the available + * values for this item. + * @return the value hints + */ + public List getValueHints() { + return this.valueHints; + } + + /** + * The value providers that are applicable to this item. Only one + * {@link ValueProvider} is enabled for an item: the first in the list that is + * supported should be used. + * @return the value providers + */ + public List getValueProviders() { + return this.valueProviders; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java new file mode 100644 index 000000000..4d73902c9 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.ide.eclipse.org.json.JSONArray; +import org.springframework.ide.eclipse.org.json.JSONObject; + +/** + * Read standard json metadata format as {@link ConfigurationMetadataRepository}. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class JsonReader { + + private static final int BUFFER_SIZE = 4096; + + private final DescriptionExtractor descriptionExtractor = new DescriptionExtractor(); + + public RawConfigurationMetadata read(Object origin, InputStream in, Charset charset) + throws IOException { + JSONObject json = readJson(in, charset); + List groups = parseAllSources(json); + List items = parseAllItems(json); + List hints = parseAllHints(json); + return new RawConfigurationMetadata(origin, groups, items, hints); + } + + private List parseAllSources(JSONObject root) { + List result = new ArrayList(); + if (!root.has("groups")) { + return result; + } + JSONArray sources = root.getJSONArray("groups"); + for (int i = 0; i < sources.length(); i++) { + JSONObject source = sources.getJSONObject(i); + result.add(parseSource(source)); + } + return result; + } + + private List parseAllItems(JSONObject root) { + List result = new ArrayList(); + if (!root.has("properties")) { + return result; + } + JSONArray items = root.getJSONArray("properties"); + for (int i = 0; i < items.length(); i++) { + JSONObject item = items.getJSONObject(i); + result.add(parseItem(item)); + } + return result; + } + + private List parseAllHints(JSONObject root) { + List result = new ArrayList(); + if (!root.has("hints")) { + return result; + } + JSONArray items = root.getJSONArray("hints"); + for (int i = 0; i < items.length(); i++) { + JSONObject item = items.getJSONObject(i); + result.add(parseHint(item)); + } + return result; + } + + private ConfigurationMetadataSource parseSource(JSONObject json) { + ConfigurationMetadataSource source = new ConfigurationMetadataSource(); + source.setGroupId(json.getString("name")); + source.setType(json.optString("type", null)); + String description = json.optString("description", null); + source.setDescription(description); + source.setShortDescription( + this.descriptionExtractor.getShortDescription(description)); + source.setSourceType(json.optString("sourceType", null)); + source.setSourceMethod(json.optString("sourceMethod", null)); + return source; + } + + private ConfigurationMetadataItem parseItem(JSONObject json) { + ConfigurationMetadataItem item = new ConfigurationMetadataItem(); + item.setId(json.getString("name")); + item.setType(json.optString("type", null)); + String description = json.optString("description", null); + item.setDescription(description); + item.setShortDescription( + this.descriptionExtractor.getShortDescription(description)); + item.setDefaultValue(readItemValue(json.opt("defaultValue"))); + item.setDeprecation(parseDeprecation(json)); + item.setSourceType(json.optString("sourceType", null)); + item.setSourceMethod(json.optString("sourceMethod", null)); + return item; + } + + private ConfigurationMetadataHint parseHint(JSONObject json) { + ConfigurationMetadataHint hint = new ConfigurationMetadataHint(); + hint.setId(json.getString("name")); + if (json.has("values")) { + JSONArray values = json.getJSONArray("values"); + for (int i = 0; i < values.length(); i++) { + JSONObject value = values.getJSONObject(i); + ValueHint valueHint = new ValueHint(); + valueHint.setValue(readItemValue(value.get("value"))); + String description = value.optString("description", null); + valueHint.setDescription(description); + valueHint.setShortDescription( + this.descriptionExtractor.getShortDescription(description)); + hint.getValueHints().add(valueHint); + } + } + if (json.has("providers")) { + JSONArray providers = json.getJSONArray("providers"); + for (int i = 0; i < providers.length(); i++) { + JSONObject provider = providers.getJSONObject(i); + ValueProvider valueProvider = new ValueProvider(); + valueProvider.setName(provider.getString("name")); + if (provider.has("parameters")) { + JSONObject parameters = provider.getJSONObject("parameters"); + Iterator keys = parameters.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + valueProvider.getParameters().put(key, + readItemValue(parameters.get(key))); + } + } + hint.getValueProviders().add(valueProvider); + } + } + return hint; + } + + private Deprecation parseDeprecation(JSONObject object) { + if (object.has("deprecation")) { + JSONObject deprecationJsonObject = object.getJSONObject("deprecation"); + Deprecation deprecation = new Deprecation(); + deprecation.setReason(deprecationJsonObject.optString("reason", null)); + deprecation + .setReplacement(deprecationJsonObject.optString("replacement", null)); + return deprecation; + } + return (object.optBoolean("deprecated") ? new Deprecation() : null); + } + + private Object readItemValue(Object value) { + if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + Object[] content = new Object[array.length()]; + for (int i = 0; i < array.length(); i++) { + content[i] = array.get(i); + } + return content; + } + return value; + } + + private JSONObject readJson(InputStream in, Charset charset) throws IOException { + try { + StringBuilder out = new StringBuilder(); + InputStreamReader reader = new InputStreamReader(in, charset); + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = reader.read(buffer)) != -1) { + out.append(buffer, 0, bytesRead); + } + return new JSONObject(out.toString()); + } + finally { + in.close(); + } + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/README.txt b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/README.txt new file mode 100644 index 000000000..f3e11fe93 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/README.txt @@ -0,0 +1,10 @@ +The source code in this package is taken from here: + +https://github.com/spring-projects/spring-boot/tree/fca6dbaf09c32202d9d958f815221aad54b9fc7b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata + +Notes: + - This commit is from the master branch at a point in time where boot team is working on Boot 1.4.x on that branch. + +There are currently no modifications being made to that code at all to accomodate STS. So it may now be possible to consume it as a proper dependency. +However, keep in mind that we are using a modified copy of 'org.json' to allow controlling key order in json maps. So that probably +complicates things. diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java new file mode 100644 index 000000000..a264b1e9b --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * A raw metadata structure. Used to initialize a {@link ConfigurationMetadataRepository}. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class RawConfigurationMetadata { + + private final Object origin; + + private final List sources; + + private final List items; + + private final List hints; + + RawConfigurationMetadata(Object parsedFrom, + List sources, + List items, + List hints) { + this.origin = parsedFrom; + this.sources = new ArrayList(sources); + this.items = new ArrayList(items); + this.hints = new ArrayList(hints); + for (ConfigurationMetadataItem item : this.items) { + resolveName(item); + } + } + + public List getSources() { + return this.sources; + } + + public ConfigurationMetadataSource getSource(String type) { + for (ConfigurationMetadataSource source : this.sources) { + if (type.equals(source.getType())) { + return source; + } + } + return null; + } + + public List getItems() { + return this.items; + } + + public List getHints() { + return this.hints; + } + + /** + * Resolve the name of an item against this instance. + * @param item the item to resolve + * @see ConfigurationMetadataProperty#setName(String) + */ + private void resolveName(ConfigurationMetadataItem item) { + item.setName(item.getId()); // fallback + if (item.getSourceType() == null) { + return; + } + ConfigurationMetadataSource source = getSource(item.getSourceType()); + if (source != null) { + String groupId = source.getGroupId(); + String dottedPrefix = groupId + "."; + String id = item.getId(); + if (hasLength(groupId) && id.startsWith(dottedPrefix)) { + String name = id.substring(dottedPrefix.length(), id.length()); + item.setName(name); + } + } + } + + private static boolean hasLength(String string) { + return (string != null && string.length() > 0); + } + + @Override + public String toString() { + if (origin!=null) { + return "RawConfigurationMetadata("+origin+")"; + } + return super.toString(); + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java new file mode 100644 index 000000000..e12ec4479 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The default {@link ConfigurationMetadataRepository} implementation. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class SimpleConfigurationMetadataRepository + implements ConfigurationMetadataRepository, Serializable { + + private final Map allGroups = new HashMap(); + + @Override + public Map getAllGroups() { + return Collections.unmodifiableMap(this.allGroups); + } + + @Override + public Map getAllProperties() { + Map properties = new HashMap(); + for (ConfigurationMetadataGroup group : this.allGroups.values()) { + properties.putAll(group.getProperties()); + } + return properties; + } + + /** + * Register the specified {@link ConfigurationMetadataSource sources}. + * @param sources the sources to add + */ + public void add(Collection sources) { + for (ConfigurationMetadataSource source : sources) { + String groupId = source.getGroupId(); + ConfigurationMetadataGroup group = this.allGroups.get(groupId); + if (group == null) { + group = new ConfigurationMetadataGroup(groupId); + this.allGroups.put(groupId, group); + } + String sourceType = source.getType(); + if (sourceType != null) { + putIfAbsent(group.getSources(), sourceType, source); + } + } + } + + /** + * Add a {@link ConfigurationMetadataProperty} with the + * {@link ConfigurationMetadataSource source} that defines it, if any. + * @param property the property to add + * @param source the source + */ + public void add(ConfigurationMetadataProperty property, + ConfigurationMetadataSource source) { + if (source != null) { + putIfAbsent(source.getProperties(), property.getId(), property); + } + putIfAbsent(getGroup(source).getProperties(), property.getId(), property); + } + + /** + * Merge the content of the specified repository to this repository. + * @param repository the repository to include + */ + public void include(ConfigurationMetadataRepository repository) { + for (ConfigurationMetadataGroup group : repository.getAllGroups().values()) { + ConfigurationMetadataGroup existingGroup = this.allGroups.get(group.getId()); + if (existingGroup == null) { + this.allGroups.put(group.getId(), group); + } + else { + // Merge properties + for (Map.Entry entry : group + .getProperties().entrySet()) { + putIfAbsent(existingGroup.getProperties(), entry.getKey(), + entry.getValue()); + } + // Merge sources + for (Map.Entry entry : group + .getSources().entrySet()) { + putIfAbsent(existingGroup.getSources(), entry.getKey(), + entry.getValue()); + } + } + } + + } + + private ConfigurationMetadataGroup getGroup(ConfigurationMetadataSource source) { + if (source == null) { + ConfigurationMetadataGroup rootGroup = this.allGroups.get(ROOT_GROUP); + if (rootGroup == null) { + rootGroup = new ConfigurationMetadataGroup(ROOT_GROUP); + this.allGroups.put(ROOT_GROUP, rootGroup); + } + return rootGroup; + } + return this.allGroups.get(source.getGroupId()); + } + + private void putIfAbsent(Map map, String key, V value) { + if (!map.containsKey(key)) { + map.put(key, value); + } + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java new file mode 100644 index 000000000..043fc10dc --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; + +/** + * Hint for a value a given property may have. Provide the value and an optional + * description. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class ValueHint implements Serializable, Cloneable { + + public static ValueHint withValue(Object value) { + ValueHint hint = new ValueHint(); + hint.setValue(value); + return hint; + } + + public ValueHint prefixWith(String prefix) { + try { + ValueHint clone = (ValueHint) this.clone(); + clone.setValue(prefix+value); + return clone; + } catch (CloneNotSupportedException e) { + //This is supposed to be impossble. + throw new RuntimeException(e); + } + } + + private Object value; + + private String description; + + private String shortDescription; + + /** + * Return the hint value. + * @return the value + */ + public Object getValue() { + return this.value; + } + + public void setValue(Object value) { + this.value = value; + } + + /** + * A description of this value, if any. Can be multi-lines. + * @return the description + * @see #getShortDescription() + */ + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * A single-line, single-sentence description of this hint, if any. + * @return the short description + * @see #getDescription() + */ + public String getShortDescription() { + return this.shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + @Override + public String toString() { + return "ValueHint{" + "value=" + this.value + ", description='" + this.description + + '\'' + '}'; + } +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java new file mode 100644 index 000000000..550181ee9 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationmetadata; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Define a component that is able to provide the values of a property. + *

+ * Each provider is defined by a {@code name} and can have an arbitrary number of + * {@code parameters}. The available providers are defined in the Spring Boot + * documentation. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@SuppressWarnings("serial") +public class ValueProvider implements Serializable { + + private String name; + + private final Map parameters = new LinkedHashMap(); + + /** + * Return the name of the provider. + * @return the name + */ + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Return the parameters. + * @return the parameters + */ + public Map getParameters() { + return this.parameters; + } + + @Override + public String toString() { + return "ValueProvider{" + "name='" + this.name + ", parameters=" + this.parameters + + '}'; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java new file mode 100644 index 000000000..e25ef89eb --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Spring Boot configuration meta-data parser. + */ +package org.springframework.boot.configurationmetadata; diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/eclipse/org/json/JSONObject.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/eclipse/org/json/JSONObject.java new file mode 100644 index 000000000..dcfbce7bc --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/eclipse/org/json/JSONObject.java @@ -0,0 +1,1663 @@ +package org.springframework.ide.eclipse.org.json; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +//This is a patched copy of the original JSONObject source code (which can be +// found in the lib directory of this project. + +//Reason for patch: use LinkedHashMap instead of HashMap so as to preserve +// key ordering in JSONObject when its deserialized and serialized. + +// The changes are marked with //CHANGED comments + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing + * the values by name, and put methods for adding or replacing + * values by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A + * JSONObject constructor can be used to convert an external form JSON text + * into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. A + * get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + *

+ * The put methods add or replace values in an object. For + * example, + * + *

+ * myString = new JSONObject()
+ *         .put("JSON", "Hello, World!").toString();
+ * 
+ * + * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a + * quote or single quote, and if they do not contain leading or trailing + * spaces, and if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, + * or null.
  • + *
+ * + * @author JSON.org + * @version 2013-06-17 + */ +public class JSONObject { + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * + * @return NULL. + */ + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object + * An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + public boolean equals(Object object) { + return object == null || object == this; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + public String toString() { + return "null"; + } + } + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + //CHANGED BEG + //this.map = new HashMap(); + this.map = createMap(); + //CHANGED END + } + + //CHANGED BEG + //added: + private Map createMap() { + return new LinkedHashMap(); + } + //CHANGED END + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo + * A JSONObject. + * @param names + * An array of strings. + * @throws JSONException + * @exception JSONException + * If a value is a non-finite number or if a name is + * duplicated. + */ + public JSONObject(JSONObject jo, String[] names) { + this(); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x + * A JSONTokener object containing the source string. + * @throws JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + +// The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + this.putOnce(key, x.nextValue()); + +// Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param map + * A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + */ + public JSONObject(Map map) { + //CHANGED BEG + //this.map = new HashMap(); + this.map = createMap(); + //CHANGED END + if (map != null) { + Iterator i = map.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + Object value = e.getValue(); + if (value != null) { + this.map.put(e.getKey(), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with "get" or + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + * + * The key is formed by removing the "get" or "is" + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + * + * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + * + * @param bean + * An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object + * An object that has fields that should be used to make a + * JSONObject. + * @param names + * An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String names[]) { + this(); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source + * A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @exception JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName + * The ResourceBundle base name. + * @param locale + * The Locale to load the ResourceBundle for. + * @throws JSONException + * If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof String) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + * + * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is an invalid number or if the key is null. + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the key is null or if the current value associated with + * the key is not a JSONArray. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d + * A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key + * A key string. + * @return The object associated with the key. + * @throws JSONException + * if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with a key. + * + * @param key + * A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + /** + * Get the double value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).doubleValue() + : Double.parseDouble((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number."); + } + } + + /** + * Get the int value associated with a key. + * + * @param key + * A key string. + * @return The integer value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).intValue() + : Integer.parseInt((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not an int."); + } + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + /** + * Get the long value associated with a key. + * + * @param key + * A key string. + * @return The long value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).longValue() + : Long.parseLong((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a long."); + } + } + + /** + * Get an array of field names from a JSONObject. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + int length = jo.length(); + if (length == 0) { + return null; + } + Iterator iterator = jo.keys(); + String[] names = new String[length]; + int i = 0; + while (iterator.hasNext()) { + names[i] = (String) iterator.next(); + i += 1; + } + return names; + } + + /** + * Get an array of field names from an Object. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key + * A key string. + * @return A string which is the value. + * @throws JSONException + * if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONObject[" + quote(key) + "] not a string."); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key + * A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if it is + * an Integer, Long, Double, or Float, then add one to it. + * + * @param key + * A key string. + * @return this. + * @throws JSONException + * If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key + * A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.keySet().iterator(); + } + + /** + * Get a set of keys of the JSONObject. + * + * @return A keySet. + */ + public Set keySet() { + return this.map.keySet(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + JSONArray ja = new JSONArray(); + Iterator keys = this.keys(); + while (keys.hasNext()) { + ja.put(keys.next()); + } + return ja.length() == 0 ? null : ja; + } + + /** + * Produce a string from a Number. + * + * @param number + * A Number + * @return A String. + * @throws JSONException + * If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + +// Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key + * A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key + * A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + try { + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + try { + return this.getDouble(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + try { + return this.getInt(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + try { + return this.getLong(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key + * A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + +// If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass + .getDeclaredMethods(); + for (int i = 0; i < methods.length; i += 1) { + try { + Method method = methods[i]; + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName(); + String key = ""; + if (name.startsWith("get")) { + if ("getClass".equals(name) + || "getDeclaringClass".equals(name)) { + key = ""; + } else { + key = name.substring(3); + } + } else if (name.startsWith("is")) { + key = name.substring(2); + } + if (key.length() > 0 + && Character.isUpperCase(key.charAt(0)) + && method.getParameterTypes().length == 0) { + if (key.length() == 1) { + key = key.toLowerCase(); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase() + + key.substring(1); + } + + Object result = method.invoke(bean, (Object[]) null); + if (result != null) { + this.map.put(key, wrap(result)); + } + } + } + } catch (Exception ignore) { + } + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A boolean which is the value. + * @return this. + * @throws JSONException + * If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key + * A key string. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Collection value) throws JSONException { + this.put(key, new JSONArray(value)); + return this; + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A double which is the value. + * @return this. + * @throws JSONException + * If the key is null or if the number is invalid. + */ + public JSONObject put(String key, double value) throws JSONException { + this.put(key, new Double(value)); + return this; + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * An int which is the value. + * @return this. + * @throws JSONException + * If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + this.put(key, new Integer(value)); + return this; + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A long which is the value. + * @return this. + * @throws JSONException + * If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + this.put(key, new Long(value)); + return this; + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key + * A key string. + * @param value + * A Map value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Map value) throws JSONException { + this.put(key, new JSONObject(value)); + return this; + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number or if the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key + * @param value + * @return his. + * @throws JSONException + * if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + this.put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + this.put(key, value); + } + return this; + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within = '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + /** + * Remove a name and its value, if present. + * + * @param key + * The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string + * A String. + * @return A simple JSON value. + */ + public static Object stringToValue(String string) { + Double d; + if (string.equals("")) { + return string; + } + if (string.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (string.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (string.equalsIgnoreCase("null")) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char b = string.charAt(0); + if ((b >= '0' && b <= '9') || b == '-') { + try { + if (string.indexOf('.') > -1 || string.indexOf('e') > -1 + || string.indexOf('E') > -1) { + d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = new Long(string); + if (string.equals(myLong.toString())) { + if (myLong.longValue() == myLong.intValue()) { + return new Integer(myLong.intValue()); + } else { + return myLong; + } + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o + * The object to test. + * @throws JSONException + * If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names + * A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException + * If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a prettyprinted JSON text of this JSONObject. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + Object object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object instanceof String) { + return (String) object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + return new JSONObject((Map) value).toString(); + } + if (value instanceof Collection) { + return new JSONArray((Collection) value).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + return quote(value.toString()); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object + * The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String) { + return object; + } + + if (object instanceof Collection) { + return new JSONArray((Collection) object); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + return new JSONObject((Map) object); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + new JSONObject((Map) value).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + new JSONArray((Collection) value).write(writer, indentFactor, + indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else if (value instanceof Number) { + writer.write(numberToString((Number) value)); + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean commanate = false; + final int length = this.length(); + Iterator keys = this.keys(); + writer.write('{'); + + if (length == 1) { + Object key = keys.next(); + writer.write(quote(key.toString())); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + writeValue(writer, this.map.get(key), indentFactor, indent); + } else if (length != 0) { + final int newindent = indent + indentFactor; + while (keys.hasNext()) { + Object key = keys.next(); + if (commanate) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newindent); + writer.write(quote(key.toString())); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + writeValue(writer, this.map.get(key), indentFactor, + newindent); + commanate = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesLoader.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesLoader.java new file mode 100644 index 000000000..c2dbe1969 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertiesLoader.java @@ -0,0 +1,164 @@ +package org.springframework.ide.vscode.boot.properties.metadata; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; + +public class PropertiesLoader { + + private static final String MAIN_SPRING_CONFIGURATION_METADATA_JSON = "META-INF/spring-configuration-metadata.json"; + + public static final String ADDITIONAL_SPRING_CONFIGURATION_METADATA_JSON = "META-INF/additional-spring-configuration-metadata.json"; + + /** + * The default classpath location for config metadata loaded when scanning .jar files on the classpath. + */ + public static final String[] JAR_META_DATA_LOCATIONS = { + MAIN_SPRING_CONFIGURATION_METADATA_JSON + //Not scanning 'additional' metadata because it integrated already in the main data. + }; + + /** + * The default classpath location for config metadata loaded when scanning project output folders. + */ + public static final String[] PROJECT_META_DATA_LOCATIONS = { + MAIN_SPRING_CONFIGURATION_METADATA_JSON, + ADDITIONAL_SPRING_CONFIGURATION_METADATA_JSON + }; + + private static final Logger LOG = Logger.getLogger(PropertiesLoader.class.getName()); + + private ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create(); + + public ConfigurationMetadataRepository load(Path projectPath) { + return load(projectPath.resolve("classpath.txt"), projectPath.resolve("target/classes")); + } + + private ConfigurationMetadataRepository load(Path classPathFilePath, Path outputFolderPath) { + loadFromClasspath(classPathFilePath); + loadFromOutputFolder(outputFolderPath); + ConfigurationMetadataRepository repository = builder.build(); + return repository; + } + + private void loadFromClasspath(Path classPathFilePath) { + if (classPathFilePath != null && Files.exists(classPathFilePath)) { + readClassPathFile(classPathFilePath).ifPresent(classPathSet -> { + classPathSet.stream().forEach(this::loadFromJar); + }); + } + } + + private Optional> readClassPathFile(Path classPathFilePath){ + try { + InputStream in = Files.newInputStream(classPathFilePath); + String text = new BufferedReader(new InputStreamReader(in)).lines().collect(Collectors.joining()); + Path dir = classPathFilePath.getParent(); + return Optional.of(Arrays.stream(text.split(File.pathSeparator)).map(dir::resolve).collect(Collectors.toSet())); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Failed to read classpath text file", e); + return Optional.empty(); + } + } + + private void loadFromOutputFolder(Path outputFolderPath) { + if (outputFolderPath != null && Files.exists(outputFolderPath)) { + Arrays.stream(PROJECT_META_DATA_LOCATIONS).forEach(mdLoc -> { + loadFromJsonFile(outputFolderPath.resolve(mdLoc)); + }); + } + } + + private void loadFromJsonFile(Path mdf) { + if (Files.exists(mdf)) { + InputStream is = null; + try { + is = Files.newInputStream(mdf); + loadFromInputStream(mdf, is); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error loading file '" + mdf + "'", e); + } finally { + if (is!=null) { + try { + is.close(); + } catch (IOException e) { + //ignore + } + } + } + } + } + + private void loadFromJar(Path f) { + JarFile jarFile = null; + try { + jarFile = new JarFile(f.toFile()); + //jarDump(jarFile); + for (String loc : JAR_META_DATA_LOCATIONS) { + ZipEntry e = jarFile.getEntry(loc); + if (e!=null) { + loadFrom(jarFile, e); + } + } + } catch (Throwable e) { + LOG.log(Level.SEVERE, "Error loading JAR file", e); + } finally { + if (jarFile!=null) { + try { + jarFile.close(); + } catch (IOException e) { + } + } + } + } + + + private void loadFrom(JarFile jarFile, ZipEntry ze) { + InputStream is = null; + try { + is = jarFile.getInputStream(ze); + loadFromInputStream(jarFile.getName()+"["+ze.getName()+"]", is); + } catch (Throwable e) { + LOG.log(Level.SEVERE, "Error loading JAR file", e); + } finally { + if (is!=null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + private void loadFromInputStream(Object origin, InputStream is) throws IOException { + builder.withJsonResource(origin, is); + } + + public static void main(String[] args) { + if (args.length > 0) { + Path projectPath = Paths.get(args[0]); + ConfigurationMetadataRepository repo = new PropertiesLoader().load(projectPath); + Map allProperties = repo.getAllProperties(); + allProperties.keySet().forEach(System.out::println); + } + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertyInfo.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertyInfo.java new file mode 100644 index 000000000..e6fd06fbe --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/PropertyInfo.java @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (c) 2014-2016 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.metadata; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataSource; +import org.springframework.boot.configurationmetadata.Deprecation; +import org.springframework.boot.configurationmetadata.ValueHint; +import org.springframework.boot.configurationmetadata.ValueProvider; +import org.springframework.ide.vscode.boot.properties.metadata.ValueProviderRegistry.ValueProviderStrategy; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +/** + * Information about a spring property, basically, this is the same as + * + * {@link ConfigurationMetadataProperty} but augmented with information + * about {@link ConfigurationMetadataSource}s that declare the property. + * + * @author Kris De Volder + */ +public class PropertyInfo { + + /** + * Identifies a 'Source'. This is essentially the sames as {@link ConfigurationMetadataSource}. + * We could use {@link ConfigurationMetadataSource} directly, but this only contains + * the info that we actually use so takes less memory. + */ + public static class PropertySource { + private final String sourceType; + private final String sourceMethod; + public PropertySource(ConfigurationMetadataSource source) { + String st = source.getSourceType(); + this.sourceType = st!=null?st:source.getType(); + this.sourceMethod = source.getSourceMethod(); + } + @Override + public String toString() { + return sourceType+"::"+sourceMethod; + } + public String getSourceType() { + return sourceType; + } + public String getSourceMethod() { + return sourceMethod; + } + } + + final private String id; + private String type; + final private String name; + final private Object defaultValue; + final private String description; + private List sources; + private Deprecation deprecation; + private ImmutableList valueHints; + private ImmutableList keyHints; + private ValueProviderStrategy valueProvider; + private ValueProviderStrategy keyProvider; + + public PropertyInfo(String id, String type, String name, + Object defaultValue, String description, + Deprecation deprecation, + List valueHints, + List keyHints, + ValueProviderStrategy valueProvider, + ValueProviderStrategy keyProvider, + List sources) { + super(); + this.id = id; + this.type = type; + this.name = name; + this.defaultValue = defaultValue; + this.description = description; + this.deprecation = deprecation; + this.valueHints = valueHints==null?null:ImmutableList.copyOf(valueHints); + this.keyHints = keyHints==null?null:ImmutableList.copyOf(keyHints); + this.valueProvider = valueProvider; + this.keyProvider = keyProvider; + this.sources = sources; + } + public PropertyInfo(ValueProviderRegistry valueProviders, ConfigurationMetadataProperty prop) { + this( + prop.getId(), + prop.getType(), + prop.getName(), + prop.getDefaultValue(), + prop.getDescription(), + prop.getDeprecation(), + prop.getHints().getValueHints(), + prop.getHints().getKeyHints(), + valueProviders.resolve(prop.getHints().getValueProviders()), + valueProviders.resolve(prop.getHints().getKeyProviders()), + null + ); + for (ValueProvider h : prop.getHints().getValueProviders()) { + if (h.getName().equals("handle-as")) { + handleAs(h.getParameters().get("target")); + } + } + } + private void handleAs(Object targetObject) { +// debug("handle-as "+this.getId()+" -> "+targetObject); + if (targetObject instanceof String) { + this.type = (String)targetObject; + } + } + public String getId() { + return id; + } + public String getType() { + return type; + } + public String getName() { + return name; + } + public Object getDefaultValue() { + return defaultValue; + } + public String getDescription() { + return description; + } + +// public HintProvider getHints(TypeUtil typeUtil, boolean dimensionAware) { +// Type type = TypeParser.parse(this.type); +// if (TypeUtil.isMap(type)) { +// return HintProviders.forMap(keyHints(typeUtil), valueHints(typeUtil), TypeUtil.getDomainType(type), dimensionAware); +// } else if (TypeUtil.isSequencable(type)) { +// if (dimensionAware) { +// if (TypeUtil.isSequencable(type)) { +// return HintProviders.forDomainAt(valueHints(typeUtil), TypeUtil.getDimensionality(type)); +// } else { +// return HintProviders.forHere(valueHints(typeUtil)); +// } +// } else { +// return HintProviders.forAllValueContexts(valueHints(typeUtil)); +// } +// } else { +// return HintProviders.forHere(valueHints(typeUtil)); +// } +// } +// +// private HintProvider keyHints(TypeUtil typeUtil) { +// return HintProviders.basic(typeUtil.getJavaProject(), keyHints, keyProvider); +// } +// +// private HintProvider valueHints(TypeUtil typeUtil) { +// return HintProviders.basic(typeUtil.getJavaProject(), valueHints, valueProvider); +// } + + public List getSources() { + if (sources!=null) { + return sources; + } + return Collections.emptyList(); + } + + @Override + public String toString() { + return "PropertyInfo("+getId()+")"; + } + public void addSource(ConfigurationMetadataSource source) { + if (sources==null) { + sources = new ArrayList(); + } + sources.add(new PropertySource(source)); + } + + public PropertyInfo withId(String alias) { + if (alias.equals(id)) { + return this; + } + return new PropertyInfo(alias, type, name, defaultValue, description, deprecation, valueHints, keyHints, valueProvider, keyProvider, sources); + } + + public void setDeprecation(Deprecation d) { + this.deprecation = d; + } + + public boolean isDeprecated() { + return deprecation!=null; + } + + public String getDeprecationReason() { + return deprecation == null ? null : deprecation.getReason(); + } + + public String getDeprecationReplacement() { + return deprecation == null ? null : deprecation.getReplacement(); + } + + public void addValueHints(List hints) { + Builder builder = ImmutableList.builder(); + builder.addAll(valueHints); + builder.addAll(hints); + valueHints = builder.build(); + } + public void addKeyHints(List hints) { + Builder builder = ImmutableList.builder(); + builder.addAll(keyHints); + builder.addAll(hints); + keyHints = builder.build(); + } +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertiesIndexManager.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertiesIndexManager.java new file mode 100644 index 000000000..a142d65fc --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertiesIndexManager.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2014 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.metadata; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.ide.vscode.boot.properties.util.FuzzyMap; +import org.springframework.ide.vscode.boot.properties.util.Listener; +import org.springframework.ide.vscode.boot.properties.util.ListenerManager; + +/** + * Support for Reconciling, Content Assist and Hover Text in spring properties + * file all make use of a per-project index of spring properties metadata extracted + * from project's classpath. This Index manager is responsible for keeping at most + * one index per-project and to keep the index up-to-date. + * + * @author Kris De Volder + */ +public class SpringPropertiesIndexManager extends ListenerManager> { + + private Map indexes = null; + final private ValueProviderRegistry valueProviders; + + public SpringPropertiesIndexManager(ValueProviderRegistry valueProviders) { + this.valueProviders = valueProviders; + } + + public synchronized FuzzyMap get(Path projectFolder) { + if (indexes==null) { + indexes = new HashMap<>(); + } + SpringPropertyIndex index = indexes.get(projectFolder); + if (index==null) { + index = new SpringPropertyIndex(valueProviders, projectFolder); + indexes.put(projectFolder, index); + } + return index; + } + + public synchronized void clear() { + if (indexes!=null) { + indexes.clear(); + for (Listener l : getListeners()) { + l.changed(this); + } + } + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertyIndex.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertyIndex.java new file mode 100644 index 000000000..5930b16de --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/SpringPropertyIndex.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2015 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.metadata; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataGroup; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataSource; +import org.springframework.ide.vscode.boot.properties.util.FuzzyMap; + +public class SpringPropertyIndex extends FuzzyMap { + + private ValueProviderRegistry valueProviders; + + public SpringPropertyIndex(ValueProviderRegistry valueProviders, Path projectPath) { + this.valueProviders = valueProviders; + if (projectPath!=null) { +// try { + PropertiesLoader loader = new PropertiesLoader(); + ConfigurationMetadataRepository metadata = loader.load(projectPath); + //^^^ Should be done in bg? It seems fast enough for now. + + Collection allEntries = metadata.getAllProperties().values(); + for (ConfigurationMetadataProperty item : allEntries) { + add(new PropertyInfo(valueProviders, item)); + } + + for (ConfigurationMetadataGroup group : metadata.getAllGroups().values()) { + for (ConfigurationMetadataSource source : group.getSources().values()) { + for (ConfigurationMetadataProperty prop : source.getProperties().values()) { + PropertyInfo info = get(prop.getId()); + info.addSource(source); + } + } + } + + // System.out.println(">>> spring properties metadata loaded "+this.size()+" items==="); + // dumpAsTestData(); + // System.out.println(">>> spring properties metadata loaded "+this.size()+" items==="); +// } catch (Exception e) { +// LOG.log +// } + } + } + + public void add(ConfigurationMetadataProperty propertyInfo) { + add(new PropertyInfo(valueProviders, propertyInfo)); + } + + /** + * Dumps out 'test data' based on the current contents of the index. This is not meant to be + * used in 'production' code. The idea is to call this method during development to dump a + * 'snapshot' of the index onto System.out. The data is printed in a forma so that it can be easily + * pasted/used into JUNit testing code. + */ + public void dumpAsTestData() { + List> allData = this.find(""); + for (Match match : allData) { + PropertyInfo d = match.data; + System.out.println("data(" + +dumpString(d.getId())+", " + +dumpString(d.getType())+", " + +dumpString(d.getDefaultValue())+", " + +dumpString(d.getDescription()) +");" + ); +// for (PropertySource source : d.getSources()) { +// String st = source.getSourceType(); +// String sm = source.getSourceMethod(); +// if (sm!=null) { +// System.out.println(d.getId() +" from: "+st+"::"+sm); +// } +// } + } + } + + private String dumpString(Object v) { + if (v==null) { + return "null"; + } + return dumpString(""+v); + } + + private String dumpString(String s) { + if (s==null) { + return "null"; + } else { + StringBuilder buf = new StringBuilder("\""); + for (char c : s.toCharArray()) { + switch (c) { + case '\r': + buf.append("\\r"); + break; + case '\n': + buf.append("\\n"); + break; + case '\\': + buf.append("\\\\"); + break; + case '\"': + buf.append("\\\""); + break; + default: + buf.append(c); + break; + } + } + buf.append("\""); + return buf.toString(); + } + } + + @Override + protected String getKey(PropertyInfo entry) { + return entry.getId(); + } +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/ValueProviderRegistry.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/ValueProviderRegistry.java new file mode 100644 index 000000000..28ddac0da --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/metadata/ValueProviderRegistry.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2016 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.metadata; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.boot.configurationmetadata.ValueProvider; +import org.springframework.ide.vscode.boot.properties.util.CollectionUtil; + +/** + * An instance of this class serves as a 'registry' that associates known + * {@link ValueProvider} ids to strategy objects used in the computation of completions + * for properties to which the provider is attached. + * + * @author Kris De Volder + */ +public class ValueProviderRegistry { + + private static ValueProviderRegistry DEFAULT; + + /** + * Creates a default {@link ValueProviderRegistry} which is initialized with all the known + * providers. (This is the one production code should use, test code might make use + * something else for mocking purposes). + */ + public synchronized static ValueProviderRegistry getDefault() { + if (DEFAULT==null) { + DEFAULT = new ValueProviderRegistry(); + DEFAULT.initializeDefaults(DEFAULT); + } + return DEFAULT; + } + + protected void initializeDefaults(ValueProviderRegistry r) { +// def("logger-name", LoggerNameProvider.FACTORY); +// def("class-reference", ClassReferenceProvider.FACTORY); + } + + private Map, ValueProviderStrategy>> registry = new HashMap<>(); + + public interface ValueProviderStrategy { +// Flux getValues(IJavaProject javaProject, String query); +// +// default Collection getValuesNow(IJavaProject javaProject, String query) { +// return this.getValues(javaProject, query) +// .take(CachingValueProvider.TIMEOUT) +// .collectList() +// .block(); +// } + } + + /** + * Defines a value provider by binding its id to a strategy. + */ + public void def(String id, Function, ValueProviderStrategy> algo) { + registry.put(id, algo); + } + + /** + * Resolve a list of {@link ValueProvider}s to a {@link ValueProviderStrategy}. + *

+ * Essentially this finds the first provider from the list which has a known name + * and uses that to iinstantiate a ValueProviderStrategy. Spring boot assumes that + * a list is provided to allow new providers to be defined that override older ones + * and these are added at the top of the list. Thus an older IDE can continue to + * function using the older provider further down the list whereas newer IDEs will + * use a 'better' one from higher up the list. + */ + public ValueProviderStrategy resolve(List providerDescriptors) { + if (CollectionUtil.hasElements(providerDescriptors)) { + for (ValueProvider descriptor : providerDescriptors) { + Function, ValueProviderStrategy> factory = registry.get(descriptor.getName()); + if (factory!=null) { + Map params = descriptor.getParameters(); + return factory.apply(params); + } + } + } + return null; + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ArrayUtils.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ArrayUtils.java new file mode 100644 index 000000000..b445b2aaa --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ArrayUtils.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2015 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.util; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @author Kris De Volder + */ +public class ArrayUtils { + + public static boolean hasElements(T[] arr) { + return arr!=null && arr.length>0; + } + + public static T lastElement(T[] arr) { + if (hasElements(arr)) { + return arr[arr.length-1]; + } + return null; + } + + + public static T firstElement(T[] arr) { + if (hasElements(arr)) { + return arr[0]; + } + return null; + } + + @SuppressWarnings("unchecked") + public static T[] remove(T[] array, T element) { + ArrayList toKeep = new ArrayList<>(Arrays.asList(array)); + toKeep.remove(element); + T[] newArray =(T[]) Array.newInstance(array.getClass().getComponentType(), toKeep.size()); + return toKeep.toArray(newArray); + } +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/CollectionUtil.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/CollectionUtil.java new file mode 100644 index 000000000..02a32792f --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/CollectionUtil.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2015 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.util; + +import java.util.Collection; + +/** + * @author Kris De Volder + */ +public class CollectionUtil { + + public static boolean hasElements(Collection c) { + return c!=null && !c.isEmpty(); + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/FuzzyMap.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/FuzzyMap.java new file mode 100644 index 000000000..9f5bcd548 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/FuzzyMap.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * Copyright (c) 2014 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.logging.Logger; + +import org.springframework.ide.vscode.util.FuzzyMatcher; + +/** + * A collection of data that can be searched with a simple 'fuzzy' string + * matching algorithm. Clients must override 'getKey' method to define how + * a search 'key' is associated with each data item. + *

+ * The collection can then be searched for items who's key matches + * simple 'fuzzy' patterns. + */ +public abstract class FuzzyMap implements Iterable { + + private static final Logger LOG = Logger.getLogger(FuzzyMap.class.getName()); + + public static class Match { + public double score; + public final E data; + private String pattern; + + public Match(String pattern, double score, E e) { + this.pattern = pattern; + this.score = score; + this.data = e; + } + public static Match getBest(Collection> matches) { + double bestScore = Double.NEGATIVE_INFINITY; + Match best = null; + for (Match match : matches) { + if (match.score>bestScore) { + best = match; + bestScore = match.score; + } + } + return best; + } + + @Override + public String toString() { + return "Match(score="+score+", data="+data+")"; + } + public String getPattern() { + return pattern; + } + } + + @Override + public Iterator iterator() { + return entries.values().iterator(); + } + + private TreeMap entries = new TreeMap(); + + protected abstract String getKey(E entry); + + public void add(E value) { + //This assumes no two entries have the same id. + String key = getKey(value); + E existing = entries.get(key); + if (existing==null) { + entries.put(getKey(value), value); + } else { + LOG.warning(FuzzyMap.class.getName()+": Multiple entries for key "+key+" some entries discarded"); + } + } + + /** + * Search for pattern. A pattern is just a sequence of characters which have to found in + * an entrie's key in the same order as they are in the pattern. + *

+ * Note that returned list doesn't yet have elements sorted according to score (instead they + * are sorted lexicographically thanks to the fact we use a Tree representation). + */ + public List> find(String pattern) { + if ("".equals(pattern)) { + //Special case because + // 1) no need to search. Matches everything + // 2) want to use different way of sorting / scoring. See https://issuetracker.springsource.com/browse/STS-4008 + ArrayList> matches = new ArrayList>(entries.size()); + for (E v : entries.values()) { + matches.add(new Match(pattern, 1.0, v)); + } + return matches; + } else { + //TODO: optimize somehow with a smarter index? (right now searches all map entries sequentially) + ArrayList> matches = new ArrayList>(); + for (Entry e : entries.entrySet()) { + String key = e.getKey(); + double score = FuzzyMatcher.matchScore(pattern, key); + if (score!=0.0) { + matches.add(new Match(pattern, score, e.getValue())); + } + } + return matches; + } + } + + /** + * Searches the index for the longest string which is both + * - a prefix of propertyName + * - a prefix of some key in the map. + * Note: If the map is empty, then this returns null, since + * no string, not even the empty string is a prefix of a + * key in the map. + */ + public String findValidPrefix(String propertyName) { + E best = findLongestCommonPrefixEntry(propertyName); + return best==null?null:StringUtil.commonPrefix(propertyName, getKey(best)); + } + + /** + * Find property with longest common prefix for given key. + */ + public E findLongestCommonPrefixEntry(String propertyName) { + //We can implementation this O(log(n)) because the properties are kept in a TreeMap which is sorted. + //This means that entries with common prefix will occur 'next to eachother' + //The 'best' entry must therefore be either the entry just before or just after + //the property we are searching for. + + Entry ceiln = entries.ceilingEntry(propertyName); + Entry floor = entries.floorEntry(propertyName); + Entry best; + if (floor==null || floor==ceiln) { + best = ceiln; + } else if (ceiln==null) { + best = floor; + } else { + int floorScore = floor==null?0:StringUtil.commonPrefixLength(floor.getKey(), propertyName); + int ceilnScore = ceiln==null?0:StringUtil.commonPrefixLength(ceiln.getKey(), propertyName); + best = floorScore>ceilnScore ? floor : ceiln; + } + return best==null?null:best.getValue(); + } + + /** + * Find an exact match if it exists. + */ + public E get(String id) { + return entries.get(id); + } + + public boolean isEmpty() { + return entries==null || entries.isEmpty(); + } + + public int size() { + return entries.size(); + } + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/Listener.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/Listener.java new file mode 100644 index 000000000..ba1ae5621 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/Listener.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2014 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.util; + +/** + * @author Kris De Volder + */ +public interface Listener { + + void changed(T info); + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ListenerManager.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ListenerManager.java new file mode 100644 index 000000000..2a4bf5c94 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/ListenerManager.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2014 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.util; + +import java.util.Arrays; + +import org.springframework.ide.vscode.util.ListenerList; + +public class ListenerManager { + + private ListenerList listeners = new ListenerList<>(ListenerList.IDENTITY); + + public void addListener(T l) { + listeners.add(l); + } + + public void removeListener(T l) { + listeners.remove(l); + } + + @SuppressWarnings("unchecked") + public Iterable getListeners() { + return (Iterable) Arrays.asList(listeners.getListeners()); + } + + + +} diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/StringUtil.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/StringUtil.java new file mode 100644 index 000000000..bade83da7 --- /dev/null +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/boot/properties/util/StringUtil.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2015 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.util; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class StringUtil { + + public static boolean hasText(String name) { + return name!=null && !name.trim().equals(""); + } + + public static String trim(String s) { + if (s!=null) { + return s.trim(); + } + return null; + } + + public static String trimEnd(String s) { + if (s!=null) { + return s.replaceAll("\\s+\\z", ""); + } + return null; + } + + public static int commonPrefixLength(CharSequence s, CharSequence t) { + int shortestStringLen = Math.min(s.length(), t.length()); + for (int i = 0; i < shortestStringLen; i++) { + if (s.charAt(i)!=t.charAt(i)) { + return i; + } + } + //no difference found upto entire length of shortest string. + return shortestStringLen; + } + + /** + * @return longest string which is a prefix of both argument Strings. + */ + public static String commonPrefix(CharSequence s, CharSequence t) { + int len = commonPrefixLength(s, t); + if (len>0) { + return s.subSequence(0,len).toString(); + } + return ""; + } + + public static String camelCaseToHyphens(String value) { + Matcher matcher = CAMEL_CASE_PATTERN.matcher(value); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, matcher.group(1) + '-' + + matcher.group(2).toLowerCase()); + } + matcher.appendTail(result); + return result.toString(); + } + + private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])"); + + public static String arrayToCommaDelimitedString(Object[] array) { + return collectionToCommaDelimitedString(Arrays.asList(array)); + } + + public static String collectionToCommaDelimitedString(Collection items) { + StringBuilder buf = new StringBuilder(); + boolean first = true; + for (Object item : items) { + if (!first) { + buf.append(","); + } + buf.append(item); + first = false; + } + return buf.toString(); + } + + public static String upperCaseToHyphens(String v) { + if (v!=null) { + return v.toLowerCase().replace('_', '-'); + } + return null; + } + + public static String hyphensToUpperCase(String v) { + if (v!=null) { + return v.toUpperCase().replace('-', '_'); + } + return null; + } + + public static String hyphensToCamelCase(String propName, boolean startWithUpperCase) { + String [] parts = propName.split("-"); + if (startWithUpperCase) { + parts[0] = upCaseFirstChar(parts[0]); + } + StringBuilder camelCased = new StringBuilder(parts[0]); + for (int i = 1; i < parts.length; i++) { + camelCased.append(upCaseFirstChar(parts[i])); + } + return camelCased.toString(); + } + + + public static String upCaseFirstChar(String string) { + if (StringUtil.hasText(string)) { + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + return ""; + } + + public static String datestamp() { + Date d = new Date(); + SimpleDateFormat f = new SimpleDateFormat("yyyyMMdd"); + return f.format(d); + } + +} diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ListenerList.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ListenerList.java new file mode 100644 index 000000000..d5f380dde --- /dev/null +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/ListenerList.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * Copyright (c) 2004, 2016 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.util; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * This class is a thread safe list that is designed for storing lists of listeners. + * The implementation is optimized for minimal memory footprint, frequent reads + * and infrequent writes. Modification of the list is synchronized and relatively + * expensive, while accessing the listeners is very fast. For legacy code, readers are given access + * to the underlying array data structure for reading, with the trust that they will + * not modify the underlying array. + *

+ * A listener list handles the same listener being added + * multiple times, and tolerates removal of listeners that are the same as other + * listeners in the list. For this purpose, listeners can be compared with each other + * using either equality or identity, as specified in the list constructor. + *

+ *

+ * Use an enhanced 'for' loop to notify listeners. The recommended + * code sequence for notifying all registered listeners of say, + * FooListener#eventHappened(Event), is: + *

+ *
+ListenerList<FooListener> fooListeners = new ListenerList<>();
+//...
+for (FooListener listener : fooListeners) {
+	listener.eventHappened(event);
+}
+ * 
+ *

+ * Legacy code may still call {@link #getListeners()} and then use a 'for' loop + * to iterate the {@code Object[]}. This might be insignificantly faster, but + * it lacks type-safety and risks inadvertent modifications to the array. + *

+ *

+ * This class can be used without OSGi running. + *

+ * + * @param the type of listeners in this list + * @since org.eclipse.equinox.common 3.2 + */ +public class ListenerList implements Iterable { + + /** + * The empty array singleton instance. + */ + private static final Object[] EmptyArray = new Object[0]; + + /** + * Mode constant (value 0) indicating that listeners should be considered + * the same if they are equal. + */ + public static final int EQUALITY = 0; + + /** + * Mode constant (value 1) indicating that listeners should be considered + * the same if they are identical. + */ + public static final int IDENTITY = 1; + + /** + * Indicates the comparison mode used to determine if two + * listeners are equivalent + */ + private final boolean identity; + + /** + * The list of listeners. Initially empty but initialized + * to an array of size capacity the first time a listener is added. + * Maintains invariant: listeners != null + */ + private volatile Object[] listeners = EmptyArray; + + /** + * Creates a listener list in which listeners are compared using equality. + */ + public ListenerList() { + this(EQUALITY); + } + + /** + * Creates a listener list using the provided comparison mode. + * + * @param mode The mode used to determine if listeners are the same. + */ + public ListenerList(int mode) { + if (mode != EQUALITY && mode != IDENTITY) + throw new IllegalArgumentException(); + this.identity = mode == IDENTITY; + } + + /** + * Adds a listener to this list. This method has no effect if the same + * listener is already registered. + * + * @param listener the non-null listener to add + */ + public synchronized void add(E listener) { + // This method is synchronized to protect against multiple threads adding + // or removing listeners concurrently. This does not block concurrent readers. + if (listener == null) + throw new IllegalArgumentException(); + // check for duplicates + final int oldSize = listeners.length; + for (int i = 0; i < oldSize; ++i) { + Object listener2 = listeners[i]; + if (identity ? listener == listener2 : listener.equals(listener2)) + return; + } + // Thread safety: create new array to avoid affecting concurrent readers + Object[] newListeners = new Object[oldSize + 1]; + System.arraycopy(listeners, 0, newListeners, 0, oldSize); + newListeners[oldSize] = listener; + //atomic assignment + this.listeners = newListeners; + } + + /** + * Returns an array containing all the registered listeners. + * The resulting array is unaffected by subsequent adds or removes. + * If there are no listeners registered, the result is an empty array. + * Use this method when notifying listeners, so that any modifications + * to the listener list during the notification will have no effect on + * the notification itself. + *

+ * Note: Callers of this method must not modify the returned array. + *

+ *

+ * Note: The recommended and type-safe way to iterate this list is to use + * an enhanced 'for' statement, see {@link ListenerList}. + * This method is deprecated for new code. + *

+ * + * @return the list of registered listeners + */ + public Object[] getListeners() { + return listeners; + } + + /** + * Returns an iterator over all the registered listeners. + * The resulting iterator is unaffected by subsequent adds or removes. + * Use this method when notifying listeners, so that any modifications + * to the listener list during the notification will have no effect on + * the notification itself. + * + * @return an iterator + * @since org.eclipse.equinox.common 3.8 + */ + @Override + public Iterator iterator() { + return new ListenerListIterator<>(listeners); + } + + private static class ListenerListIterator implements Iterator { + private Object[] listeners; + private int i; + + public ListenerListIterator(Object[] listeners) { + this.listeners = listeners; + } + + @Override + public boolean hasNext() { + return i < listeners.length; + } + + @Override + public E next() { + if (i >= listeners.length) { + throw new NoSuchElementException(); + } + @SuppressWarnings("unchecked") // (E) is safe, because #add(E) only accepts Es + E next = (E) listeners[i++]; + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * Returns whether this listener list is empty. + * + * @return true if there are no registered listeners, and + * false otherwise + */ + public boolean isEmpty() { + return listeners.length == 0; + } + + /** + * Removes a listener from this list. Has no effect if the same + * listener was not already registered. + * + * @param listener the non-null listener to remove + */ + public synchronized void remove(Object listener) { + // This method is synchronized to protect against multiple threads adding + // or removing listeners concurrently. This does not block concurrent readers. + if (listener == null) + throw new IllegalArgumentException(); + int oldSize = listeners.length; + for (int i = 0; i < oldSize; ++i) { + Object listener2 = listeners[i]; + if (identity ? listener == listener2 : listener.equals(listener2)) { + if (oldSize == 1) { + listeners = EmptyArray; + } else { + // Thread safety: create new array to avoid affecting concurrent readers + Object[] newListeners = new Object[oldSize - 1]; + System.arraycopy(listeners, 0, newListeners, 0, i); + System.arraycopy(listeners, i + 1, newListeners, i, oldSize - i - 1); + //atomic assignment to field + this.listeners = newListeners; + } + return; + } + } + } + + /** + * Returns the number of registered listeners. + * + * @return the number of registered listeners + */ + public int size() { + return listeners.length; + } + + /** + * Removes all listeners from this list. + */ + public synchronized void clear() { + listeners = EmptyArray; + } +}