PdeManifestImportUpdater.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.core.cleanupapp;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Adds one package to the main OSGi {@code Import-Package} header without reformatting existing clauses. */
public final class PdeManifestImportUpdater {

	private static final String IMPORT_PACKAGE_HEADER= "Import-Package:"; //$NON-NLS-1$
	private static final Pattern IMPORT_PACKAGE_PATTERN= Pattern.compile(
			"(?m)^Import-Package:[^\\r\\n]*(?:\\r?\\n [^\\r\\n]*)*"); //$NON-NLS-1$
	private static final Pattern MAIN_SECTION_SEPARATOR= Pattern.compile("\\r?\\n\\r?\\n"); //$NON-NLS-1$
	private static final Pattern PACKAGE_NAME= Pattern.compile(
			"[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*"); //$NON-NLS-1$

	private PdeManifestImportUpdater() {
	}

	public static byte[] addImport(byte[] original, String packageName) {
		Objects.requireNonNull(original);
		String content= new String(original, StandardCharsets.UTF_8);
		String updated= addImport(content, packageName);
		return content.equals(updated) ? original : updated.getBytes(StandardCharsets.UTF_8);
	}

	public static String addImport(String manifest, String packageName) {
		Objects.requireNonNull(manifest);
		Objects.requireNonNull(packageName);
		if (!PACKAGE_NAME.matcher(packageName).matches()) {
			throw new IllegalArgumentException("Invalid Java package name: " + packageName); //$NON-NLS-1$
		}

		int mainSectionEnd= mainSectionEnd(manifest);
		Matcher header= IMPORT_PACKAGE_PATTERN.matcher(manifest.substring(0, mainSectionEnd));
		String lineDelimiter= lineDelimiter(manifest);
		if (header.find()) {
			String physicalHeader= header.group();
			String unfolded= physicalHeader.substring(IMPORT_PACKAGE_HEADER.length())
					.replace("\r\n ", "").replace("\n ", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
			if (containsPackage(unfolded, packageName)) {
				return manifest;
			}
			String separator= physicalHeader.stripTrailing().endsWith(",") //$NON-NLS-1$
					? lineDelimiter + " " : "," + lineDelimiter + " "; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			return manifest.substring(0, header.end()) + separator + packageName
					+ manifest.substring(header.end());
		}

		String prefix= mainSectionEnd == 0 || isLineBreakBefore(manifest, mainSectionEnd)
				? "" : lineDelimiter; //$NON-NLS-1$
		String suffix= mainSectionEnd == manifest.length() && isLineBreakBefore(manifest, mainSectionEnd)
				? lineDelimiter : ""; //$NON-NLS-1$
		return manifest.substring(0, mainSectionEnd) + prefix + IMPORT_PACKAGE_HEADER + " " //$NON-NLS-1$
				+ packageName + suffix + manifest.substring(mainSectionEnd);
	}

	private static int mainSectionEnd(String manifest) {
		Matcher separator= MAIN_SECTION_SEPARATOR.matcher(manifest);
		return separator.find() ? separator.start() : manifest.length();
	}

	private static boolean isLineBreakBefore(String value, int offset) {
		if (offset <= 0) {
			return false;
		}
		char previous= value.charAt(offset - 1);
		return previous == '\n' || previous == '\r';
	}

	private static String lineDelimiter(String manifest) {
		return manifest.contains("\r\n") ? "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	private static boolean containsPackage(String headerValue, String packageName) {
		for (String clause : splitOutsideQuotes(headerValue, ',')) {
			for (String segment : splitOutsideQuotes(clause, ';')) {
				String candidate= segment.trim();
				if (candidate.contains(":=") || candidate.indexOf('=') >= 0) { //$NON-NLS-1$
					break;
				}
				if (packageName.equals(candidate)) {
					return true;
				}
			}
		}
		return false;
	}

	private static List<String> splitOutsideQuotes(String value, char delimiter) {
		List<String> result= new ArrayList<>();
		int start= 0;
		boolean quoted= false;
		boolean escaped= false;
		for (int index= 0; index < value.length(); index++) {
			char current= value.charAt(index);
			if (escaped) {
				escaped= false;
				continue;
			}
			if (quoted && current == '\\') {
				escaped= true;
				continue;
			}
			if (current == '"') {
				quoted= !quoted;
				continue;
			}
			if (!quoted && current == delimiter) {
				result.add(value.substring(start, index));
				start= index + 1;
			}
		}
		result.add(value.substring(start));
		return result;
	}
}