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

org.conqat.engine.resource.util.UniformPathUtils Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) CQSE GmbH
 *
 * 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.conqat.engine.resource.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.uniformpath.UniformPath;

/**
 * Utility methods for dealing with uniform paths.
 */
public class UniformPathUtils {

	/**
	 * Matches windows drive letters prefixes, e.g. "C:/", and captures the path after the drive letter
	 * as first group. Requires the path to be normalized before matching, as the pattern will not match
	 * '\'.
	 */
	public static final Pattern DRIVE_LETTER_PATTERN = Pattern.compile("[A-Za-z]:/(.*)");

	/** The character used as path separator in uniform paths. */
	public static final char SEPARATOR_CHAR = '/';

	/** String representation of {@link #SEPARATOR_CHAR}. */
	public static final String SEPARATOR = String.valueOf(SEPARATOR_CHAR);

	/** The file extension of a architecture definition file */
	public static final String ARCHITECTURE_FILE_EXTENSION = ".architecture";

	/**
	 * Extracts the project part of a uniform path, which is everything up to the first
	 * {@link #SEPARATOR_CHAR}.
	 */
	public static String extractProject(String uniformPath) {
		return StringUtils.getFirstParts(uniformPath, 1, SEPARATOR_CHAR);
	}

	/**
	 * Returns the path without the project, i.e. removes everything up to the first
	 * {@link #SEPARATOR_CHAR}.
	 */
	public static String stripProject(String uniformPath) {
		int pos = uniformPath.indexOf(SEPARATOR_CHAR);
		if (pos >= 0) {
			return uniformPath.substring(pos + 1);
		}
		return uniformPath;
	}

	/**
	 * Returns the element name for a uniform path, which is everything starting from the last
	 * {@link #SEPARATOR_CHAR}.
	 */
	public static String getElementName(String uniformPath) {
		return StringUtils.getLastPart(uniformPath, SEPARATOR_CHAR);
	}

	/**
	 * Returns the parent path for a path which is everything up to the last non-escaped
	 * {@link #SEPARATOR_CHAR}. If no separator is found, the empty string is returned.
	 */
	public static String getParentPath(String uniformPath) {
		// we can not use StringUtils.removeLastPart(), as the behavior for a
		// string without separator is different here
		int lastSlash = uniformPath.length();
		while ((lastSlash = uniformPath.lastIndexOf(SEPARATOR_CHAR, lastSlash - 1)) != -1) {
			if (lastSlash > 0 && uniformPath.charAt(lastSlash - 1) != '\\') {
				break;
			}
		}
		if (lastSlash == -1) {
			return StringUtils.EMPTY_STRING;
		}
		return uniformPath.substring(0, lastSlash);
	}

	/** Removes the first count segments from the given path. */
	public static String removeFirstSegments(String uniformPath, int count) {
		String[] segments = splitPath(uniformPath);
		return concatenate(Arrays.copyOfRange(segments, count, segments.length));
	}

	/** Removes the last count segments from the given path. */
	public static String removeLastSegments(String uniformPath, int count) {
		String[] segments = splitPath(uniformPath);
		return concatenate(Arrays.copyOfRange(segments, 0, segments.length - count));
	}

	/**
	 * Returns segments forming the given path. This takes escaped slashes into consideration, so
	 * foo/bar\/great -> ["foo", "bar\/great"]. Also removes leading slashes.
	 */
	public static String[] splitPath(String uniformPath) {
		int currentOffset = -1;
		List splitPositions = new ArrayList<>();
		while ((currentOffset = uniformPath.indexOf(SEPARATOR_CHAR, currentOffset + 1)) != -1) {
			if (currentOffset == 0
					|| (currentOffset <= uniformPath.length() - 1 && uniformPath.charAt(currentOffset - 1) != '\\')) {
				splitPositions.add(currentOffset);
			}
		}

		if (splitPositions.isEmpty()) {
			return new String[] { uniformPath };
		}

		String[] segments = new String[splitPositions.size() + 1];
		for (int i = 0; i < splitPositions.size(); i++) {
			if (i == 0) {
				segments[i] = uniformPath.substring(0, splitPositions.get(i));
			} else {
				segments[i] = uniformPath.substring(splitPositions.get(i - 1) + 1, splitPositions.get(i));
			}
			if (i == splitPositions.size() - 1) {
				segments[i + 1] = uniformPath.substring(splitPositions.get(i) + 1);
			}

		}
		return segments;
	}

	/**
	 * Returns the extension of the uniform path.
	 * 
	 * @return File extension, i.e. "java" for "FileSystemUtils.java", or null, if the path
	 *         has no extension (i.e. if a path contains no '.'), returns the empty string if the '.' is
	 *         the path's last character.
	 */
	public static @Nullable String getExtension(String uniformPath) {
		String name = getElementName(uniformPath);
		int posLastDot = name.lastIndexOf('.');
		if (posLastDot < 0) {
			return null;
		}
		return name.substring(posLastDot + 1);
	}

	/** Returns whether the file with the given path is an architecture file */
	public static boolean isArchitectureFile(String uniformPath) {
		return !uniformPath.startsWith(UniformPath.EType.ARCHITECTURE.getPrefix())
				&& ARCHITECTURE_FILE_EXTENSION.equals("." + getExtension(uniformPath));
	}

	/**
	 * Returns whether the file with the given path is an architecture root component i.e. a first level
	 * child of -architectures-.
	 */
	public static boolean isArchitectureRootComponent(String uniformPath) {
		return uniformPath.startsWith(UniformPath.EType.ARCHITECTURE.getPrefix())
				&& ARCHITECTURE_FILE_EXTENSION.equals("." + getExtension(uniformPath));
	}

	/**
	 * Replaces forward and backward slashes, not only system-specific separators, with a forward slash.
	 * We do this on purpose, since paths that e.g. are read from files do not necessarily contain the
	 * separators contained in File.separator. Multiple slashes (regardless of type) in a row are
	 * collapsed to one. If nothing is changed, the input string is returned. 
*
* Implementation Details:
*

* To avoid using regexes for the normalization, for performance reasons, this method uses a lower * level algorithm, that is much faster (>5x) than the same regex based operation. The idea is that * we create a output char[] into which we copy all the characters that we want to keep, one by one, * thus making this operation O(N) and requiring very little allocations. To keep track of where we * are in the result array, we need to keep a tab on the amount skipped characters, to keep copying * into the next free slot, after skipping a char. In the end we return a subcopy of the result * char[], to remove the dangling empty chars in the array, if we removed something. *

*/ public static String normalizeAllSeparators(String path) { int pathLength = path.length(); char[] result = new char[pathLength]; int skippedChars = 0; boolean previousWasPathSeparator = false; boolean madeChanges = false; for (int i = 0; i < pathLength; i++) { char currentChar = path.charAt(i); switch (currentChar) { case '\\': madeChanges = true; // fallthrough intended case SEPARATOR_CHAR: if (previousWasPathSeparator) { // skip the current path separator since it is redundant madeChanges = true; skippedChars += 1; continue; } result[i - skippedChars] = SEPARATOR_CHAR; previousWasPathSeparator = true; break; default: result[i - skippedChars] = currentChar; previousWasPathSeparator = false; } } if (madeChanges) { return new String(Arrays.copyOfRange(result, 0, result.length - skippedChars)); } else { return path; } } /** * Creates a clean path by resolving duplicate slashes, single and double dots. This is the * equivalent to path canonization on uniform paths. */ public static String cleanPath(String path) { String[] parts = splitPath(path); for (int i = 0; i < parts.length; ++i) { // do not use StringUtils.isEmpty(), as we do not want trim // semantics! if (StringUtils.EMPTY_STRING.equals(parts[i]) || ".".equals(parts[i])) { parts[i] = null; } else if ("..".equals(parts[i])) { // cancel last non-null (if any) int j = i - 1; for (; j >= 0; --j) { if ("..".equals(parts[j])) { // another '..' acts as boundary break; } if (parts[j] != null) { // cancel both parts of the path. parts[j] = null; parts[i] = null; break; } } } } return joinPath(parts); } /** Joins the given array as a path, but ignoring null entries. */ private static String joinPath(String[] parts) { StringBuilder sb = new StringBuilder(); for (String part : parts) { if (part != null) { if (sb.length() > 0) { sb.append(SEPARATOR_CHAR); } sb.append(part); } } return sb.toString(); } /** * For a uniform path denoting a file and a relative path, constructs the uniform path for the * relatively addressed element. */ public static String resolveRelativePath(String basePath, String relative) { // obtain "directory" from path denoting file String directory = getParentPath(basePath); if (!directory.isEmpty()) { directory += SEPARATOR; } return cleanPath(directory + relative); } /** * Returns the concatenated path of all given parts. Empty or null strings are ignored. */ public static String concatenate(String... parts) { List list = new ArrayList<>(parts.length); for (String part : parts) { if (!StringUtils.isEmpty(part)) { list.add(part); } } return StringUtils.concat(list, SEPARATOR); } /** * Remove drive letters or unix root slash from path. Also normalizes the path. */ public static String createSystemIndependentPath(String path) { Matcher m1 = DRIVE_LETTER_PATTERN.matcher(normalizeAllSeparators(path)); if (m1.matches()) { // Remove drive letter path = m1.group(1); } // Remove unix root slash path = StringUtils.stripPrefix(path, "/"); return path; } /** * Reduces the provided {@code uniformPaths} to their common prefix. All elements must be of the * same {@link UniformPath#getType() type}. */ public static @NonNull UniformPath reduceToCommonPrefix(List uniformPaths) { return uniformPaths.stream().reduce(UniformPathUtils::getCommonPrefix) .orElseThrow(() -> new IllegalArgumentException("uniformPaths must not be empty")); } /** * Computes the common prefix of the provided {@link UniformPath}s. Both must be at least of the * same {@link UniformPath#getType() type}. */ public static @NonNull UniformPath getCommonPrefix(UniformPath p1, UniformPath p2) { if (p1.getType() != p2.getType()) { throw new IllegalArgumentException( String.format("Cannot find a common prefix for different uniform path types: %s (%s) and %s (%s)", p1.getType(), p1, p2.getType(), p2)); } List segments = new ArrayList<>(); Iterator p1Segments = p1.getPathSegments().iterator(); Iterator p2Segments = p2.getPathSegments().iterator(); while (p1Segments.hasNext() && p2Segments.hasNext()) { String p1Value = p1Segments.next(); String p2Value = p2Segments.next(); if (Objects.equals(p1Value, p2Value)) { segments.add(p1Value); } else { break; } } return UniformPath.of(p1.getType(), segments); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy