All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nextflow.extension.Bolts.groovy Maven / Gradle / Ivy

Go to download

A DSL modelled around the UNIX pipe concept, that simplifies writing parallel and scalable pipelines in a portable manner

There is a newer version: 24.08.0-edge
Show newest version
/*
 * Copyright 2013-2024, Seqera Labs
 *
 * 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 nextflow.extension

import java.nio.file.Path
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.locks.Lock
import java.util.regex.Pattern

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.Memoized
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FirstParam
import nextflow.file.FileHelper
import nextflow.file.FileMutex
import nextflow.util.CheckHelper
import nextflow.util.Duration
import nextflow.util.MemoryUnit
import nextflow.util.RateUnit
import org.apache.commons.lang.StringUtils
import org.codehaus.groovy.runtime.DefaultGroovyMethods
import org.codehaus.groovy.runtime.GStringImpl
import org.codehaus.groovy.runtime.ResourceGroovyMethods
import org.codehaus.groovy.runtime.StringGroovyMethods
import org.slf4j.Logger
/**
 * Generic extensions
 *
 * See more about extension methods
 * http://docs.codehaus.org/display/GROOVY/Creating+an+extension+module
 *
 * @author Paolo Di Tommaso 
 */
@CompileStatic
class Bolts {

    static public final String DATETIME_FORMAT = 'dd-MM-yyyy HH:mm'

    static private Pattern PATTERN_RIGHT_TRIM = ~/\s+$/

    static private Pattern PATTERN_LEFT_TRIM = ~/^\s+/

    @Memoized
    static private ThreadLocal getLocalDateFormat(String fmt, TimeZone tz) {

        return new ThreadLocal() {
            @Override
            protected DateFormat initialValue() {
                def result = new SimpleDateFormat(fmt, Locale.ENGLISH)
                if(tz) result.setTimeZone(tz)
                return result
            }
        }
    }

    /**
     * Format a {@link Date} object
     *
     * @param self The {@link Date} object to format
     * @param format The date format to use eg. {@code dd-MM-yyyy HH:mm}.
     * @param tz The timezone to be used eg. {@code UTC}. If {@code null} the current timezone is used.
     * @return The date-time formatted as a string
     */
    static String format(Date self, String format=null, String tz=null) {
        TimeZone zone = tz ? TimeZone.getTimeZone(tz) : null
        getLocalDateFormat(format ?: DATETIME_FORMAT, zone).get().format(self)
    }

    static String format(OffsetDateTime self, String format) {
        return self.format(DateTimeFormatter.ofPattern(format).withLocale(Locale.ENGLISH))
    }

    /**
     * Format a {@link Date} object
     *
     * @param self The {@link Date} object to format
     * @param format The date format to use eg. {@code dd-MM-yyyy HH:mm}
     * @param tz The timezone to be used. If {@code null} the current timezone is used.
     * @return The date-time formatted as a string
     */
    static String format(Date self, String format, TimeZone tz) {
        getLocalDateFormat(format ?: DATETIME_FORMAT, tz).get().format(self)
    }

    static List pairs(Map self, Map opts=null) {
        def flat = opts?.flat == true
        def result = []
        for( Map.Entry entry : self.entrySet() ) {
            if( flat && entry.value instanceof Collection )
                entry.value.iterator().each { result << [entry.key, it] }
            else
                result << [entry.key, entry.value]
        }

        return result
    }

    /**
     * Remove the left side after a dot (including it) e.g.
     * 
     *     0.10     => 0
     *     10000.00 => 10000
     * 
* * @param self * @return */ static String trimDotZero(String self) { int p = self?.indexOf('.') p!=-1 ? self.substring(0,p) : self } /** * Remove blank chars at the end of the string * * @param self The string itself * @return The string with blanks removed */ static String rightTrim(String self) { self.replaceAll(PATTERN_RIGHT_TRIM,'') } /** * Remove blank chars at string beginning * * @param self The string itself * @return The string with blanks removed */ static String leftTrim( String self ) { self.replaceAll(PATTERN_LEFT_TRIM,'') } /** *

Strips any of a set of characters from the start and end of a String. * This is similar to {@link String#trim()} but allows the characters * to be stripped to be controlled.

* *

A null input String returns null. * An empty string ("") input returns the empty string.

* *

If the stripChars String is null, whitespace is * stripped as defined by {@link Character#isWhitespace(char)}. * Alternatively use {@link #strip(String)}.

* *
     * StringUtils.strip(null, *)          = null
     * StringUtils.strip("", *)            = ""
     * StringUtils.strip("abc", null)      = "abc"
     * StringUtils.strip("  abc", null)    = "abc"
     * StringUtils.strip("abc  ", null)    = "abc"
     * StringUtils.strip(" abc ", null)    = "abc"
     * StringUtils.strip("  abcyx", "xyz") = "  abc"
     * 
* * @param str the String to remove characters from, may be null * @param stripChars the characters to remove, null treated as whitespace * @return the stripped String, null if null String input */ static String strip( String self, String stripChars = null ) { StringUtils.strip(self, stripChars) } /** *

Strips any of a set of characters from the start of a String.

* *

A null input String returns null. * An empty string ("") input returns the empty string.

* *

If the stripChars String is null, whitespace is * stripped as defined by {@link Character#isWhitespace(char)}.

* *
     * StringUtils.stripStart(null, *)          = null
     * StringUtils.stripStart("", *)            = ""
     * StringUtils.stripStart("abc", "")        = "abc"
     * StringUtils.stripStart("abc", null)      = "abc"
     * StringUtils.stripStart("  abc", null)    = "abc"
     * StringUtils.stripStart("abc  ", null)    = "abc  "
     * StringUtils.stripStart(" abc ", null)    = "abc "
     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
     * 
* * @param str the String to remove characters from, may be null * @param stripChars the characters to remove, null treated as whitespace * @return the stripped String, null if null String input */ static String stripStart( String self, String stripChars = null ) { StringUtils.stripStart(self, stripChars) } /** *

Strips any of a set of characters from the end of a String.

* *

A null input String returns null. * An empty string ("") input returns the empty string.

* *

If the stripChars String is null, whitespace is * stripped as defined by {@link Character#isWhitespace(char)}.

* *
     * StringUtils.stripEnd(null, *)          = null
     * StringUtils.stripEnd("", *)            = ""
     * StringUtils.stripEnd("abc", "")        = "abc"
     * StringUtils.stripEnd("abc", null)      = "abc"
     * StringUtils.stripEnd("  abc", null)    = "  abc"
     * StringUtils.stripEnd("abc  ", null)    = "abc"
     * StringUtils.stripEnd(" abc ", null)    = " abc"
     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
     * StringUtils.stripEnd("120.00", ".0")   = "12"
     * 
* * @param str the String to remove characters from, may be null * @param stripChars the set of characters to remove, null treated as whitespace * @return the stripped String, null if null String input */ static String stripEnd( String self, String stripChars = null ) { StringUtils.stripEnd(self, stripChars) } /** *

Capitalizes a String changing the first letter to title case as * per {@link Character#toTitleCase(char)}. No other letters are changed.

* *

For a word based algorithm, see {@link org.apache.commons.lang.WordUtils#capitalize(String)}. * A null input String returns null.

* *
     * StringUtils.capitalize(null)  = null
     * StringUtils.capitalize("")    = ""
     * StringUtils.capitalize("cat") = "Cat"
     * StringUtils.capitalize("cAt") = "CAt"
     * 
* */ static String capitalize(String self) { StringUtils.capitalize(self) } /** *

Uncapitalizes a String changing the first letter to title case as * per {@link Character#toLowerCase(char)}. No other letters are changed.

* *

For a word based algorithm, see {@link org.apache.commons.lang.WordUtils#uncapitalize(String)}. * A null input String returns null.

* *
     * StringUtils.uncapitalize(null)  = null
     * StringUtils.uncapitalize("")    = ""
     * StringUtils.uncapitalize("Cat") = "cat"
     * StringUtils.uncapitalize("CAT") = "cAT"
     * 
* */ static String uncapitalize(String self) { StringUtils.uncapitalize(self) } /** * Check if a alphabetic characters in a string are lowercase. Non alphabetic characters are ignored * @param self The string to check * @return {@true} if the string contains no uppercase characters, {@code false} otherwise */ static boolean isLowerCase(String self) { if( self ) for( int i=0; i T withLock( Lock self, boolean interruptible = false, Closure closure ) { // acquire the lock if( interruptible ) self.lockInterruptibly() else self.lock() try { return closure.call() } finally { self.unlock(); } } /** * Invokes the specify closure only if it is able to acquire a lock * * @param self * @param interruptible * @param closure * @return the closure result */ static boolean tryLock( Lock self, Closure closure ) { if( !self.tryLock() ) return false try { closure.call() } finally { self.unlock() return true } } /** * Creates a file system wide lock that prevent two or more JVM instances/process * to work on the same file * * Note: this does not protected against multiple-thread accessing the file in a * concurrent manner. * * @param * self The file over which define the lock * @param * timeout An option timeout elapsed which the a {@link InterruptedException} is thrown * @param * closure The action to apply during the lock file spawn * @return * The user provided {@code closure} result * * @throws * InterruptedException if the lock cannot be acquired within the specified {@code timeout} */ static withLock(File self, Duration timeout = null, Closure closure) { def locker = new FileMutex(self) if( timeout ) locker.setTimeout(timeout) locker.lock(closure) } /** * Creates a file system wide lock that prevent two or more JVM instances/process * to work on the same file * * Note: this does not protected against multiple-thread accessing the file in a * concurrent manner. * * @param * self The file over which define the lock * @param * timeout An option timeout elapsed which the a {@link InterruptedException} is thrown * @param * closure The action to apply during the lock file spawn * @return * The user provided {@code closure} result * * @throws * InterruptedException if the lock cannot be acquired within the specified {@code timeout} */ static withLock( Path self, Duration timeout, Closure closure ) { def locker = new FileMutex(self.toFile()) if( timeout ) locker.setTimeout(timeout) locker.lock(closure) } static Duration toDuration(Number number) { new Duration(number.toLong()) } static MemoryUnit toMemory(Number number) { new MemoryUnit(number.toLong()) } /** * Converts a {@code String} to a {@code Duration} object * * @param self * @param type * @return */ static Object asType( String self, Class type ) { if( type == Duration ) { return new Duration(self) } else if( Path.isAssignableFrom(type) ) { return FileHelper.asPath(self) } else if( type == MemoryUnit ) { return new MemoryUnit(self) } else if( type == RateUnit ) { return new RateUnit(self) } else if ( type == URL ) { return new URL(self) } else if ( type == URI ) { return URI.create(self) } StringGroovyMethods.asType(self, type); } /** * Converts a {@code GString} to a {@code Duration} object * * @param self * @param type * @return */ static Object asType( GString self, Class type ) { if( type == Duration ) { return new Duration(self.toString()) } else if( Path.isAssignableFrom(type) ) { return FileHelper.asPath(self) } else if( type == MemoryUnit ) { return new MemoryUnit(self.toString()) } else if ( type == URL ) { return new URL(self.toString()) } else if ( type == URI ) { return URI.create(self.toString()) } StringGroovyMethods.asType(self, type); } /** * Converts a {@code Number} to a {@code Duration} object * * @param self * @param type * @return */ static Object asType( Number self, Class type ) { if( type == Duration ) { return new Duration(self.longValue()) } if( type == MemoryUnit ) { return new MemoryUnit(self.longValue()) } DefaultGroovyMethods.asType(self, type); } /** * Converts a {@code File} to a {@code Path} object * * @param self * @param type * @return */ static Object asType( File self, Class type ) { if( Path.isAssignableFrom(type) ) { return self.toPath() } ResourceGroovyMethods.asType(self, type); } static V getOrCreate( Map self, K key, @ClosureParams(FirstParam.FirstGenericType) Closure value ) { getOrCreate0(self,key,value) } static V getOrCreate( Map self, K key, V value ) { getOrCreate0(self,key,value) } static private V getOrCreate0(Map self, K key, value) { if( self.containsKey(key) ) return self.get(key) synchronized (self) { if( self.containsKey(key) ) return self.get(key) final result = (V)(value instanceof Closure ? value.call(key) : value) self.put(key,result) return result } } /** * Navigate a map of maps traversing multiple attribute using dot notation. For example: * {@code x.y.z } * * @param self The root map object * @param key A dot separated list of keys * @param closure An optional closure to be applied. Only if all keys exists * @return The value associated to the specified key(s) or null on first missing entry */ static def navigate( Map self, String key, Closure closure = null ) { assert key def items = key.split(/\./) def current = self.get(items[0]) for( int i=1; i if( value instanceof Map ) { result.put( key, toConfigObject((Map)value) ) } else { result.put( key, value ) } } return result } static private normalize0( config ) { if( config instanceof Map ) { Map result = new LinkedHashMap(config.size()) config.keySet().each { name -> def value = (config as Map).get(name) result.put(name, normalize0(value)) } return result } else if( config instanceof Collection ) { List result = new ArrayList(config.size()) for( entry in config ) { result << normalize0(entry) } return result } else if( config instanceof GString ) { return config.toString() } else { return config } } /** * Indent each line in the given test by a specified prefix * * @param text * @param prefix * @return The string indented */ static String indent( String text, String prefix = ' ' ) { def result = new StringBuilder() def lines = text ? text.readLines() : Collections.emptyList() for( int i=0; i bestMatches( Collection options, String sample ) { assert sample assert options // Otherwise look for the most similar Map diffs = [:] options.each { diffs[it] = StringUtils.getLevenshteinDistance(sample, it) } // sort the Levenshtein Distance and get the fist entry def sorted = diffs.sort { Map.Entry it -> it.value } def nearest = (Map.Entry)sorted.find() int min = nearest.value int len = sample.length() int threshold = len<=3 ? 1 : ( len > 10 ? 5 : Math.floorDiv(len,2)) List result if( min <= threshold ) { result = (List)sorted.findAll { it.value==min } .collect { it.key } } else { result = [] } return result } static boolean isCamelCase(String str) { if( !str ) return false for( int i=0; i T cloneWith( T self, binding ) { def copy = (Closure)self.clone() if( binding != null ) { copy.setDelegate(binding) copy.setResolveStrategy( Closure.DELEGATE_FIRST ) } return (T)copy } /** * Create a copy of a {@link GString} object cloning all values that are instance of {@link Closure} * * @param self The {@link GString} itself * @param binding A {@link Map} object that is set as delegate object in the cloned closure. * @return The cloned {@link GString} instance */ static GString cloneAsLazy(GString self, binding) { def values = new Object[ self.valueCount ] // clone the GString setting the delegate for each closure argument for( int i=0; i closest(Collection options, String sample ) { assert sample if( !options ) return Collections.emptyList() // Otherwise look for the most similar def diffs = [:] options.each { diffs[it] = StringUtils.getLevenshteinDistance(sample, it) } // sort the Levenshtein Distance and get the fist entry def sorted = diffs.sort { it.value } def nearest = sorted.find() def min = nearest.value def len = sample.length() def threshold = len<=3 ? 1 : ( len > 10 ? 5 : Math.floor(len/2)) def result if( min <= threshold ) { result = sorted.findAll { it.value==min } .collect { it.key } } else { result = [] } return result } private static HashMap LOGGER_CACHE = new LinkedHashMap() { protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 10_000 } } private static final Duration LOG_DFLT_THROTTLE = Duration.of('1min') static synchronized private checkLogCache( Object msg, Map params, Closure action ) { // -- check if this message has already been printed final String str = msg.toString() final Throwable error = params?.causedBy as Throwable final Duration throttle = params?.throttle as Duration ?: LOG_DFLT_THROTTLE final firstOnly = params?.firstOnly == true final key = params?.cacheKey ?: str long now = System.currentTimeMillis() Long ts = LOGGER_CACHE.get(key) if( ts && (now - ts <= throttle.toMillis() || firstOnly) ) { return } LOGGER_CACHE.put(key, now) action.call(str, error) } private static Map LOGGER_PARAMS = [ cacheKey: Object, causedBy: Throwable, throttle: [String, Number, Duration], firstOnly: Boolean ] /** * Append a `trace` level entry in the application log. * * @param log * The {@link Logger} object * @param params * Optional named parameters * - `causedBy`: A {@link Throwable} object that raised the error * - `throttle`: When specified suppress identical logs within the specified time {@link Duration} * @param msg * The message to print * */ static void trace1(Logger log, Map params=null, Object msg ) { CheckHelper.checkParams('trace1', params, LOGGER_PARAMS) if( !log.isTraceEnabled() || msg==null ) return checkLogCache(msg,params) { String str, Throwable t -> t ? log.trace(str,t) : log.trace(str) } } /** * Append a `debug` level entry in the application log. * * @param log * The {@link Logger} object * @param params * Optional named parameters * - `causedBy`: A {@link Throwable} object that raised the error * - `throttle`: When specified suppress identical logs within the specified time {@link Duration} * @param msg * The message to print * */ static void debug1(Logger log, Map params=null, Object msg ) { CheckHelper.checkParams('debug1', params, LOGGER_PARAMS) if( !log.isDebugEnabled() || msg==null ) return checkLogCache(msg,params) { String str, Throwable t -> t ? log.debug(str,t) : log.debug(str) } } /** * Append a `info` level entry in the application log. * * @param log * The {@link Logger} object * @param params * Optional named parameters * - `causedBy`: A {@link Throwable} object that raised the error * - `throttle`: When specified suppress identical logs within the specified time {@link Duration} * @param msg * The message to print * */ static void info1(Logger log, Map params=null, Object msg ) { CheckHelper.checkParams('info1', params, LOGGER_PARAMS) if( !log.isInfoEnabled() || msg==null ) return checkLogCache(msg,params) { String str, Throwable t -> t ? log.info(str,t) : log.info(str) } } /** * Append a `warn` level entry in the application log. * * @param log * The {@link Logger} object * @param params * Optional named parameters * - `causedBy`: A {@link Throwable} object that raised the error * - `throttle`: When specified suppress identical logs within the specified time {@link Duration} * @param msg * The message to print * */ static void warn1(Logger log, Map params=null, Object msg ) { CheckHelper.checkParams('warn1', params, LOGGER_PARAMS) if( !log.isWarnEnabled() || msg==null ) return checkLogCache(msg,params) { String str, Throwable t -> t ? log.warn(str,t) : log.warn(str) } } /** * Append a `error` level entry in the application log. * * @param log * The {@link Logger} object * @param params * Optional named parameters * - `causedBy`: A {@link Throwable} object that raised the error * - `throttle`: When specified suppress identical logs within the specified time {@link Duration} * @param msg * The message to print * */ static void error1(Logger log, Map params=null, Object msg ) { CheckHelper.checkParams('error1', params, LOGGER_PARAMS) if( !log.isErrorEnabled() || msg==null ) return checkLogCache(msg,params) { String str, Throwable t -> t ? log.error(str,t) : log.error(str) } } static void trace(Logger log, Object msg) { if( log.isTraceEnabled() ) { log.trace(msg.toString()) } } static void trace(Logger log, Object msg, Throwable e) { if( log.isTraceEnabled() ) { log.trace(msg.toString(),e) } } @Deprecated static redact(String self, int max=5, String suffix='...') { if( !self ) return self if( self.size() T deepClone0(T obj) { final buffer = new ByteArrayOutputStream() final oos = new ObjectOutputStream(buffer) oos.writeObject(obj) oos.flush() final inputStream = new ByteArrayInputStream(buffer.toByteArray()) return (T) new ObjectInputStream(inputStream).readObject() } static T deepClone(T map) { if( map == null) return null final result = map instanceof LinkedHashMap ? new LinkedHashMap<>(map) : new HashMap<>(map) for( def key : map.keySet() ) { def value = map.get(key) if( value instanceof Map ) { result.put(key, deepClone(value)) } } return (T)result } static Map deepMerge(Map target, Map source) { final result = cloneMap0(target) for (Object name : source.keySet()) { // to prevent side-effects with ConfigObject object (which creates a value on-fly // when getting it, always use `containsKey` before if( result.containsKey(name) && result.get(name) instanceof Map && source.containsKey(name) && source.get(name) instanceof Map ) { result.put(name, deepMerge( (Map)result.get(name), (Map)source.get(name))) } else { result.put(name, source.get(name)) } } return result } static private Map cloneMap0(Map map) { if( map instanceof ConfigObject ) return ((ConfigObject)map).clone() if( map instanceof LinkedHashMap ) return new LinkedHashMap<>(map) else return new HashMap<>(map) } }