/** * Copyright 2015 StreamSets Inc. * * Licensed under the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 com.streamsets.pipeline.lib.el; import com.streamsets.pipeline.api.ElFunction; import com.streamsets.pipeline.api.ElParam; import com.streamsets.pipeline.api.el.ELEval; import com.streamsets.pipeline.api.impl.Utils; import org.apache.commons.lang3.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.SecureRandom; import java.util.Map; import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; public class StringEL { public static final String MEMOIZED = "memoized"; private static final Logger LOG = LoggerFactory.getLogger(StringEL.class); private StringEL() { } @ElFunction( prefix = "str", name = "substring", description = "Returns a new string that is a substring of this string. " + "The substring begins at the specified beginIndex and extends to the character at index endIndex-1. " + "Thus the length of the substring is endIndex-beginIndex") public static String substring( @ElParam("string") String string, @ElParam("beginIndex") int beginIndex, @ElParam("endIndex") int endIndex) { Utils.checkArgument(beginIndex >= 0, "Argument beginIndex should be 0 or greater"); Utils.checkArgument(endIndex >= 0, "Argument endIndex should be 0 or greater"); if(string == null || string.isEmpty()) { return string; } int length = string.length(); if(beginIndex > length) { return ""; } if(endIndex > string.length()) { endIndex = string.length(); } return string.substring(beginIndex, endIndex); } @ElFunction( prefix = "str", name = "trim", description = "Removes leading and trailing whitespaces") public static String trim( @ElParam("string") String string) { return string.trim(); } @ElFunction( prefix = "str", name = "toUpper", description = "Converts all of the characters in the argument string to uppercase") public static String toUpper( @ElParam("string") String string) { return string.toUpperCase(); } @ElFunction( prefix = "str", name = "toLower", description = "Converts all of the characters in the argument string to lowercase") public static String toLower( @ElParam("string") String string) { return string.toLowerCase(); } @ElFunction( prefix = "str", name = "replace", description = "Returns a new string resulting from replacing all occurrences of oldString in this string with newString") public static String replace( @ElParam("string") String string, @ElParam("oldString") String oldString, @ElParam("newString") String newString) { return string.replace(oldString, newString); } @ElFunction( prefix = "str", name = "replaceAll", description = "Replaces each substring of this string that matches the given regEx with the given replacement") public static String replaceAll( @ElParam("string") String string, @ElParam("regEx") String regEx, @ElParam("replacement") String replacement) { return string.replaceAll(regEx, replacement); } @ElFunction( prefix = "str", name = "truncate", description = "Truncates the argument string to the given index") public static String truncate( @ElParam("string") String string, @ElParam("endIndex") int endIndex) { if (string == null){ return ""; } if (endIndex < 0) { throw new IllegalArgumentException(String.format("Unable to truncate '%s' at index %s", string, endIndex)); } if (endIndex > string.length()){ LOG.warn("Attempted to truncate '{}' at index {}. Returning '{}'", string, endIndex, string); return string; } return string.substring(0, endIndex); } @SuppressWarnings("unchecked") @ElFunction( prefix = "str", name = "regExCapture", description = "Captures the string that matches the argument regular expression and the group") public static String regExCapture( @ElParam("string") String string, @ElParam("regEx") String regEx, @ElParam("groupNumber") int groupNumber) { Utils.checkArgument(regEx != null, "Argument regEx for str:regExCapture() cannot be null."); if (string != null) { Map<String, Pattern> patterns = (Map<String, Pattern>) ELEval.getVariablesInScope().getContextVariable(MEMOIZED); Matcher matcher = getPattern(patterns, regEx).matcher(string); if (matcher.find()) { return matcher.group(groupNumber); } } return null; } private static Pattern getPattern(Map<String, Pattern> patterns, String regEx) { if (patterns != null && patterns.containsKey(regEx)) { return patterns.get(regEx); } else { Pattern pattern = Pattern.compile(regEx); if (patterns != null) { patterns.put(regEx, pattern); } return pattern; } } @ElFunction( prefix = "str", name = "contains", description = "Indicates whether the argument string contains the specified substring.") public static boolean contains( @ElParam("string") String string, @ElParam("substring") String substring) { return string.contains(substring); } @ElFunction( prefix = "str", name = "startsWith", description = "Indicates whether the argument string starts with the specified prefix") public static boolean startsWith( @ElParam("string") String string, @ElParam("prefix") String prefix) { return string.startsWith(prefix); } @ElFunction( prefix = "str", name = "endsWith", description = "Indicates whether argument string ends with the specified suffix") public static boolean endsWith( @ElParam("string") String string, @ElParam("suffix") String suffix) { return string.endsWith(suffix); } @ElFunction( prefix = "str", name = "matches", description = "Tells whether the argument string matches the argument regex.") public static boolean matches( @ElParam("string") String string, @ElParam("regex") String regEx) { Utils.checkArgument(regEx != null, "Argument regEx for str:matches() cannot be null."); return string != null && string.matches(regEx); } @ElFunction( prefix = "str", name = "concat", description = "Returns a new string that is a concatenation of the two argument strings.") public static String concat( @ElParam("string1") String string1, @ElParam("string2") String string2) { string1 = (string1 == null)? "" : string1; string2 = (string2 == null)? "" : string2; return string1.concat(string2); } @ElFunction( prefix = "str", name = "length", description = "Returns the string length of the string argument." ) public static int len ( @ElParam("string") String string ) { string = (string == null)? "" : string; return string.length(); } @ElFunction( prefix = "str", name = "urlEncode", description = "Returns URL encoded variant of the string." ) public static String urlEncode ( @ElParam("string") String string, @ElParam("encoding") String encoding ) throws UnsupportedEncodingException { if(string == null) { return null; } return URLEncoder.encode(string, encoding); } @ElFunction( prefix = "str", name = "urlDecode", description = "Returns decoded string from URL encoded variant." ) public static String urlDecode ( @ElParam("string") String string, @ElParam("encoding") String encoding ) throws UnsupportedEncodingException { if(string == null) { return null; } return URLDecoder.decode(string, encoding); } @ElFunction( prefix = "str", name = "escapeXML10", description = "Returns a string safe to embed in an XML 1.0 or 1.1 document." ) public static String escapeXml10(@ElParam("string") String string) { return StringEscapeUtils.escapeXml10(string); } @ElFunction( prefix = "str", name = "escapeXML11", description = "Returns a string safe to embed in an XML 1.1 document." ) public static String escapeXml11(@ElParam("string") String string) { return StringEscapeUtils.escapeXml11(string); } @ElFunction( prefix = "str", name = "unescapeXML", description = "Returns an unescaped string from a string with XML special characters escaped." ) public static String unescapeXml(@ElParam("string") String string) { return StringEscapeUtils.unescapeXml(string); } @ElFunction( prefix = "str", name = "unescapeJava", description = "Returns an unescaped string from a string with Java special characters (e.g. \\n will be converted to 0x0A)." ) public static String java(@ElParam("string") String string) { return StringEscapeUtils.unescapeJava(string); } /* * As generating UUID is expensive operation, we spawn a thread that will pre-generate them */ static private SecureRandom randomGenerator = new SecureRandom(); static private BlockingQueue<String> uuidQueue = new ArrayBlockingQueue<>(Integer.parseInt(System.getProperty("com.streamsets.pipeline.lib.el.StringEL.uuid_queue_max", "10000"))); static private ExecutorService executor = Executors.newFixedThreadPool(1); static { executor.submit(() -> { Thread.currentThread().setName("UUID Pre generation thread"); while(true) { try { uuidQueue.put(UUID.nameUUIDFromBytes(randomGenerator.generateSeed(16)).toString()); } catch (InterruptedException e) { // Ignored } } }); } @ElFunction( prefix = "uuid", name = "uuid", description = "Returns a randomly generated UUID." ) public static String uuid() throws InterruptedException { return uuidQueue.take(); } }