/**
* The MIT License
* Copyright (c) 2014 JMXTrans Team
*
* 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 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.
*/
package org.jmxtrans.core.config;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.jmxtrans.core.circuitbreaker.CircuitBreakerProxy;
import org.jmxtrans.core.config.jaxb.InvocationType;
import org.jmxtrans.core.config.jaxb.Jmxtrans;
import org.jmxtrans.core.config.jaxb.OutputWriterType;
import org.jmxtrans.core.config.jaxb.QueriesType;
import org.jmxtrans.core.config.jaxb.QueryType;
import org.jmxtrans.core.config.jaxb.ServerType;
import org.jmxtrans.core.log.Logger;
import org.jmxtrans.core.log.LoggerFactory;
import org.jmxtrans.core.monitoring.ObjectNameFactory;
import org.jmxtrans.core.output.MetricCollectingOutputWriter;
import org.jmxtrans.core.output.OutputWriter;
import org.jmxtrans.core.output.OutputWriterFactory;
import org.jmxtrans.core.query.InProcessServer;
import org.jmxtrans.core.query.Invocation;
import org.jmxtrans.core.query.Query;
import org.jmxtrans.core.query.QueryAttribute;
import org.jmxtrans.core.query.RemoteServer;
import org.jmxtrans.core.results.MetricType;
import org.jmxtrans.utils.io.Resource;
import org.jmxtrans.utils.io.StandardResource;
import org.jmxtrans.utils.time.Clock;
import org.jmxtrans.utils.time.Interval;
import org.jmxtrans.utils.time.SystemClock;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
import static org.jmxtrans.core.results.MetricType.UNKNOWN;
@ThreadSafe
public class XmlConfigParser implements ConfigParser {
private final Logger logger = LoggerFactory.getLogger(getClass().getName());
public static final String JMXTRANS_XSD_PATH = "classpath:jmxtrans.xsd";
public static final int MAX_FAILURES = 5;
public static final int DISABLE_DURATION_MILLIS = 60 * 1000;
@Nonnull private final Clock clock;
@Nonnull private final PropertyPlaceholderResolverXmlPreprocessor preprocessor;
@Nonnull private final DocumentBuilder documentBuilder;
@Nonnull private final Unmarshaller unmarshaller;
@Nonnull private final ObjectNameFactory outputObjectNameFactory;
private XmlConfigParser(
@Nonnull DocumentBuilder documentBuilder,
@Nonnull Unmarshaller unmarshaller,
@Nonnull PropertyPlaceholderResolverXmlPreprocessor preprocessor,
@Nonnull Clock clock,
@Nonnull ObjectNameFactory outputObjectNameFactory) {
this.documentBuilder = documentBuilder;
this.unmarshaller = unmarshaller;
this.preprocessor = preprocessor;
this.clock = clock;
this.outputObjectNameFactory = outputObjectNameFactory;
}
@Override
public boolean supports(@Nonnull Resource resource) {
return resource.getPath().endsWith(".xml");
}
@Override
@Nonnull
public synchronized Configuration parseConfiguration(Resource source) throws IOException, SAXException, JAXBException, IllegalAccessException, ClassNotFoundException, InstantiationException, MalformedObjectNameException {
try (InputStream in = source.getInputStream()) {
Document document = documentBuilder.parse(in);
document = preprocessor.preprocess(document);
Jmxtrans jmxtrans = (Jmxtrans) unmarshaller.unmarshal(document);
ModifiableConfiguration newConfiguration = new ModifiableConfiguration();
parse(jmxtrans, newConfiguration);
return new StandardConfiguration(newConfiguration);
}
}
private void parse(@Nonnull Jmxtrans jmxtrans, @Nonnull ModifiableConfiguration configuration) throws IllegalAccessException, InstantiationException, ClassNotFoundException, MalformedObjectNameException {
if (jmxtrans.getCollectIntervalInSeconds() != null) {
configuration.setPeriod(new Interval(jmxtrans.getCollectIntervalInSeconds(), SECONDS));
}
if (jmxtrans.getQueries() != null) {
configuration.addServer(new InProcessServer(parse(jmxtrans.getQueries())));
}
if (jmxtrans.getServers() != null) {
try {
parse(jmxtrans.getServers(), configuration);
} catch (MalformedURLException e) {
logger.error("JMXUrl is not valid", e);
}
}
if (jmxtrans.getInvocations() != null) {
parse(jmxtrans.getInvocations(), configuration);
}
if (jmxtrans.getOutputWriters() != null) {
parse(jmxtrans.getOutputWriters(), configuration);
}
}
private void parse(Jmxtrans.Servers servers, ModifiableConfiguration configuration) throws MalformedURLException {
for (ServerType server : servers.getServer()) {
Collection<Query> queries = Collections.emptyList();
if (server.getQueries() != null) {
queries = parse(server.getQueries());
}
configuration.addServer(RemoteServer.builder()
.withUrl(server.getJmxUrl())
.withHost(server.getHost())
.withPort(server.getPort())
.withUsername(server.getUsername())
.withPassword(server.getPassword())
.withProtocolProviderPackages(server.getProtocolProviderPackages())
.withQueries(queries)
.build());
}
}
private List<Query> parse(@Nonnull QueriesType queries) {
List<Query> result = new ArrayList<>();
for (QueryType query : queries.getQuery()) {
Query.Builder queryBuilder = Query.builder()
.withObjectName(query.getObjectName())
.withResultAlias(query.getResultAlias())
.withMaxResults(query.getMaxResults());
for (QueryType.QueryAttribute attribute : query.getQueryAttribute()) {
QueryAttribute.Builder attributeBuilder = QueryAttribute
.builder(attribute.getName())
.withResultAlias(attribute.getResultAlias())
.withType(parseMetricType(attribute.getType()));
for (String key : attribute.getKey()) {
attributeBuilder.addKey(key);
}
queryBuilder.addAttribute(attributeBuilder.build());
}
result.add(queryBuilder.build());
}
return result;
}
private MetricType parseMetricType(String type) {
if (type == null) return UNKNOWN;
if (type.isEmpty()) return UNKNOWN;
return MetricType.valueOf(type.toUpperCase());
}
private void parse(@Nonnull Jmxtrans.Invocations invocations, @Nonnull ModifiableConfiguration configuration) throws MalformedObjectNameException {
for (InvocationType invocation : invocations.getInvocation()) {
List<String> params = new ArrayList<>();
List<String> signature = new ArrayList<>();
for (InvocationType.Parameter parameter : invocation.getParameter()) {
params.add(parameter.getValue());
signature.add(parameter.getType());
}
configuration.getInvocations().add(
new Invocation(
new ObjectName(invocation.getObjectName()),
invocation.getOperationName(),
params.toArray(),
signature.toArray(new String[0]),
invocation.getResultAlias(),
parseMetricType(invocation.getType()),
new SystemClock()));
}
}
private void parse(@Nonnull Jmxtrans.OutputWriters outputWriters, @Nonnull ModifiableConfiguration configuration) throws IllegalAccessException, ClassNotFoundException, InstantiationException, MalformedObjectNameException {
for (OutputWriterType outputWriter : outputWriters.getOutputWriter()) {
Map<String, String> settings = new HashMap<>();
for (Map.Entry<QName, String> attribute : outputWriter.getOtherAttributes().entrySet()) {
settings.put(attribute.getKey().getLocalPart(), attribute.getValue());
}
configuration.getOutputWriters().add(instantiateOutputWriter(outputWriter.getClazz(), settings));
}
}
@Nonnull
private OutputWriter instantiateOutputWriter(@Nonnull String outputWriterClass, @Nonnull Map<String, String> settings)
throws InstantiationException, IllegalAccessException, MalformedObjectNameException {
try {
@SuppressWarnings("unchecked")
Class<OutputWriterFactory<?>> builderClass = (Class<OutputWriterFactory<?>>) Class.forName(outputWriterClass);
OutputWriterFactory<?> builder = builderClass.newInstance();
return wrapInMetricCollectingOutputWriter(
wrapInCircuitBreaker(
builder.create(settings)));
} catch (ClassNotFoundException e) {
throw new JmxtransConfigurationException(
format("Could not load class %s, this can happen if you use non standard outputwriters and did not" +
" add the appropriate jar on the classpath", outputWriterClass), e);
}
}
private OutputWriter wrapInMetricCollectingOutputWriter(OutputWriter outputWriter) throws MalformedObjectNameException {
ObjectName objectName = outputObjectNameFactory.create(outputWriter.toString());
return new MetricCollectingOutputWriter(clock, outputWriter, objectName);
}
private OutputWriter wrapInCircuitBreaker(OutputWriter target) {
return CircuitBreakerProxy.create(
clock,
OutputWriter.class,
target,
MAX_FAILURES,
DISABLE_DURATION_MILLIS);
}
@Nonnull
public static XmlConfigParser newInstance(
@Nonnull PropertyPlaceholderResolverXmlPreprocessor preprocessor,
@Nonnull Clock clock,
@Nonnull ObjectNameFactory outputObjectNameFactory) throws JAXBException, ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
JAXBContext jaxbContext = JAXBContext.newInstance(Jmxtrans.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setSchema(loadSchema());
return new XmlConfigParser(
dbf.newDocumentBuilder(),
unmarshaller,
preprocessor,
clock,
outputObjectNameFactory);
}
@Nonnull
private static Schema loadSchema() throws SAXException, IOException {
Resource xsd = new StandardResource(JMXTRANS_XSD_PATH);
SchemaFactory schemaFactory = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
try (InputStream in = xsd.getInputStream()) {
return schemaFactory.newSchema(new StreamSource(in));
}
}
}