94b729d283
Move the java code to the new java directory The poms are now maven 2 compatible Change-Id: Idcc33bc27d202d64ffe4cbd528de6142db7dd841
258 lines
7.1 KiB
Java
258 lines
7.1 KiB
Java
/*
|
|
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
|
*
|
|
* 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 monasca.api.infrastructure.persistence.influxdb;
|
|
|
|
import org.joda.time.DateTime;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.net.URLDecoder;
|
|
import java.net.URLEncoder;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.TreeSet;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import monasca.api.domain.model.common.Paged;
|
|
|
|
final class Utils {
|
|
|
|
// Serie names match this pattern.
|
|
private static final Pattern serieNamePattern = Pattern.compile("^.+\\?.+&.+(&.+=.+)*$");
|
|
static final String COULD_NOT_LOOK_UP_COLUMNS_EXC_MSG = "Couldn't look up columns";
|
|
|
|
private Utils() {
|
|
}
|
|
|
|
/**
|
|
* InfluxDB Utilities for protecting against SQL injection attacks.
|
|
*/
|
|
static class SQLSanitizer {
|
|
|
|
private SQLSanitizer() {
|
|
}
|
|
|
|
private static final Pattern sqlUnsafePattern = Pattern.compile("^.*('|;)+.*$");
|
|
|
|
static String sanitize(final String taintedString) throws Exception {
|
|
Matcher m = sqlUnsafePattern.matcher(taintedString);
|
|
if (m.matches()) {
|
|
throw new Exception(String.format("Input from user contains single quote ['] or " +
|
|
"semi-colon [;] characters[ %1$s ]", taintedString));
|
|
}
|
|
|
|
return taintedString;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* InfluxDB Utilities for building parts of where clauses.
|
|
*/
|
|
static class WhereClauseBuilder {
|
|
|
|
private WhereClauseBuilder() {
|
|
}
|
|
|
|
static String buildTimePart(final DateTime startTime, final DateTime endTime) {
|
|
final StringBuilder sb = new StringBuilder();
|
|
|
|
if (startTime != null) {
|
|
sb.append(String.format(" and time > %1$ds", startTime.getMillis() / 1000));
|
|
}
|
|
|
|
if (endTime != null) {
|
|
sb.append(String.format(" and time < %1$ds", endTime.getMillis() / 1000));
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
}
|
|
|
|
static String buildSerieNameRegex(final String tenantId, String region, final String name,
|
|
final Map<String, String> dimensions) throws Exception {
|
|
|
|
final StringBuilder regex = new StringBuilder("^");
|
|
|
|
regex.append(urlEncodeUTF8(tenantId));
|
|
regex.append("\\?");
|
|
regex.append(urlEncodeUTF8(region));
|
|
|
|
// Name is optional.
|
|
if (name != null) {
|
|
regex.append("&");
|
|
regex.append(urlEncodeUTF8(name));
|
|
regex.append("(&|$)");
|
|
}
|
|
|
|
// Dimensions are optional.
|
|
if (dimensions != null && !dimensions.isEmpty()) {
|
|
|
|
// We depend on the fact that dimensions are sorted in the series name.
|
|
final TreeSet<Dimension> dimSortedSet = buildSortedDimSet(dimensions);
|
|
|
|
for (final Dimension dim : dimSortedSet) {
|
|
regex.append("(.*&)*");
|
|
regex.append(urlEncodeUTF8(dim.name));
|
|
regex.append("=");
|
|
regex.append(urlEncodeUTF8(dim.value));
|
|
regex.append("(&|$)");
|
|
}
|
|
}
|
|
return regex.toString();
|
|
}
|
|
|
|
static String urlEncodeUTF8(String s) throws Exception {
|
|
return URLEncoder.encode(s, "UTF-8");
|
|
}
|
|
|
|
|
|
static String urlDecodeUTF8(final String s) throws UnsupportedEncodingException {
|
|
return URLDecoder.decode(s, "UTF-8");
|
|
}
|
|
|
|
static TreeSet<Dimension> buildSortedDimSet(final Map<String, String> dimMap) {
|
|
|
|
final TreeSet<Dimension> dimTreeSet = new TreeSet<>();
|
|
for (final String dimName : dimMap.keySet()) {
|
|
final Dimension dim = new Dimension(dimName, dimMap.get(dimName));
|
|
dimTreeSet.add(dim);
|
|
}
|
|
return dimTreeSet;
|
|
}
|
|
|
|
static final class Dimension implements Comparable<Dimension> {
|
|
|
|
final String name;
|
|
final String value;
|
|
|
|
private Dimension(final String name, final String value) {
|
|
this.name = name;
|
|
this.value = value;
|
|
}
|
|
|
|
@Override
|
|
public int compareTo(Dimension o) {
|
|
int nameCmp = String.CASE_INSENSITIVE_ORDER.compare(name, o.name);
|
|
return (nameCmp != 0 ? nameCmp : String.CASE_INSENSITIVE_ORDER.compare(value, o.value));
|
|
}
|
|
}
|
|
|
|
static class SerieNameDecoder {
|
|
|
|
private final String serieName;
|
|
private final String metricName;
|
|
private final String tenantId;
|
|
private final String region;
|
|
private final Map<String, String> dimensions;
|
|
|
|
SerieNameDecoder(final String serieName) throws Exception {
|
|
|
|
if (!isSerieMetricName(serieName)) {
|
|
throw new SerieNameDecodeException ("Serie name is not decodable: " + serieName);
|
|
}
|
|
|
|
this.serieName = serieName;
|
|
|
|
this.tenantId = urlDecodeUTF8(serieName.substring(0, serieName.indexOf('?')));
|
|
String rest = serieName.substring(serieName.indexOf('?') + 1);
|
|
|
|
this.region = urlDecodeUTF8(rest.substring(0, rest.indexOf('&')));
|
|
rest = rest.substring(rest.indexOf('&') + 1);
|
|
|
|
if (rest.contains("&")) {
|
|
this.metricName = urlDecodeUTF8(rest.substring(0, rest.indexOf('&')));
|
|
rest = rest.substring(rest.indexOf('&') + 1);
|
|
} else {
|
|
this.metricName = urlDecodeUTF8(rest);
|
|
rest = null;
|
|
}
|
|
|
|
// It's possible to have no dimensions.
|
|
this.dimensions = new HashMap<>();
|
|
while (rest != null) {
|
|
final String nameValPair;
|
|
if (rest.contains("&")) {
|
|
nameValPair = rest.substring(0, rest.indexOf('&'));
|
|
rest = rest.substring(rest.indexOf('&') + 1);
|
|
} else {
|
|
nameValPair = rest;
|
|
rest = null;
|
|
}
|
|
final String dimName = urlDecodeUTF8(nameValPair.split("=")[0]);
|
|
final String dimVal = urlDecodeUTF8(nameValPair.split("=")[1]);
|
|
this.dimensions.put(dimName, dimVal);
|
|
|
|
}
|
|
}
|
|
|
|
public String getSerieName() {
|
|
return serieName;
|
|
}
|
|
|
|
public String getMetricName() {
|
|
return metricName;
|
|
}
|
|
|
|
public String getTenantId() {
|
|
return tenantId;
|
|
}
|
|
|
|
public String getRegion() {
|
|
return region;
|
|
}
|
|
|
|
public Map<String, String> getDimensions() {
|
|
return dimensions;
|
|
}
|
|
|
|
}
|
|
|
|
static class SerieNameDecodeException extends Exception {
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
public SerieNameDecodeException(String s) {
|
|
super(s);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We might come across other series that are created by the persister or don't pertain to metric
|
|
* data. They will break the parsing. Throw them away.
|
|
*/
|
|
|
|
static boolean isSerieMetricName(String serieName) {
|
|
final Matcher m = serieNamePattern.matcher(serieName);
|
|
return m.matches();
|
|
}
|
|
|
|
public static String buildOffsetPart(String offset) {
|
|
|
|
if (offset != null) {
|
|
|
|
if (!offset.isEmpty()) {
|
|
|
|
return " and time < " + offset + "ms limit " + Paged.LIMIT;
|
|
} else {
|
|
return " limit " + Paged.LIMIT;
|
|
}
|
|
|
|
} else {
|
|
return "";
|
|
}
|
|
|
|
}
|
|
}
|
|
|