FormatterServiceDelegate.java

/*
 * Copyright (c) 2020 bahlef.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * Contributors:
 * markiewb - initial API and implementation and/or initial documentation
 * Saad Mufti <saad.mufti@teamaol.com>
 * bahlef
 */
package de.funfried.netbeans.plugins.external.formatter;

import java.util.Collection;
import java.util.Objects;
import java.util.SortedSet;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;

import javax.swing.text.Document;
import javax.swing.text.StyledDocument;

import org.apache.commons.lang3.tuple.Pair;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;

import de.funfried.netbeans.plugins.external.formatter.exceptions.FormattingFailedException;
import de.funfried.netbeans.plugins.external.formatter.ui.options.Settings;

/**
 * Singleton delegation class for calling the activated external formatter, if no external formatter is
 * activated nothing will be done by this implementation. There is no fallback to the internal NetBeans
 * formatter in here!
 *
 * @author markiewb
 * @author bahlef
 */
public class FormatterServiceDelegate {
	/** {@link Logger} of this class. */
	private static final Logger log = Logger.getLogger(FormatterServiceDelegate.class.getName());

	/** {@link ReentrantLock} to synchronize the singleton instance creation. */
	private static final ReentrantLock lock = new ReentrantLock();

	/** * Singleton instance of {@link FormatterServiceDelegate}. */
	private static FormatterServiceDelegate instance = null;

	/**
	 * Private contructor due to singleton pattern.
	 */
	private FormatterServiceDelegate() {
	}

	/**
	 * Returns the singleton instance of {@link FormatterServiceDelegate}.
	 *
	 * @return the singleton instance
	 */
	@NonNull
	public static FormatterServiceDelegate getInstance() {
		lock.lock();

		try {
			if (instance == null) {
				instance = new FormatterServiceDelegate();
			}

			return instance;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Formats the given {@link StyledDocument} in regard to the given {@code changedElements}.
	 *
	 * @param document the {@link StyledDocument} which should be formatted
	 * @param changedElements a {@link SortedSet} containing ranges as {@link Pair} objects that should be formatted
	 *
	 * @return {@code true} if and only if a external formatter was found to handle the given
	 *         {@link StyledDocument} even if there was an error while formatting, otherwise
	 *         and if an external formatter delegates formatting to the NetBeans formatter it
	 *         is {@code false}
	 */
	public boolean format(StyledDocument document, SortedSet<Pair<Integer, Integer>> changedElements) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				try {
					return formatterService.format(document, changedElements);
				} catch (FormattingFailedException ex) {
					log.log(Level.INFO, formatterService.getDisplayName() + " failed to format the code", ex);
				} catch (Exception ex) {
					log.log(Level.WARNING, formatterService.getDisplayName() + " failed to format the code", ex);
				}

				return true;
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return false;
	}

	/**
	 * Returns the continuation indent size configured by the formatter which is
	 * activated for the given {@link Document}, or {@code null} if the internal
	 * NetBeans code formatter is used for the given {@code document}.
	 *
	 * @param document the {@link Document} for which the continuation indent size
	 *        is requested
	 *
	 * @return the continuation indent size configured by the formatter which is
	 *         activated for the given {@link Document}, or {@code null} if the
	 *         internal NetBeans code formatter is used for the given {@code document}
	 */
	@CheckForNull
	public Integer getContinuationIndentSize(Document document) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				return formatterService.getContinuationIndentSize(document);
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return null;
	}

	/**
	 * Returns the indent size configured by the formatter which is activated for
	 * the given {@link Document}, or {@code null} if the internal NetBeans code
	 * formatter is used for the given {@code document}.
	 *
	 * @param document the {@link Document} for which the indent size is requested
	 *
	 * @return the indent size configured by the formatter which is activated for
	 *         the given {@link Document}, or {@code null} if the internal NetBeans
	 *         code formatter is used for the given {@code document}
	 */
	@CheckForNull
	public Integer getIndentSize(Document document) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				return formatterService.getIndentSize(document);
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return null;
	}

	/**
	 * Returns the right margin (position of the red line in the editor) configured
	 * by the formatter which is activated for the given {@link Document}, or
	 * {@code null} if the internal NetBeans code formatter is used for the given
	 * {@code document}.
	 *
	 * @param document the {@link Document} for which the right margin is requested
	 *
	 * @return the right margin (position of the red line in the editor) configured
	 *         by the formatter which is activated for the given {@link Document},
	 *         or {@code null} if the internal NetBeans code formatter is used for
	 *         the given {@code document}
	 */
	@CheckForNull
	public Integer getRightMargin(Document document) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				return formatterService.getRightMargin(document);
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return null;
	}

	/**
	 * Returns the spaces per tab configured by the formatter which is activated
	 * for the given {@link Document}, or {@code null} if the internal NetBeans
	 * code formatter is used for the given {@code document}.
	 *
	 * @param document the {@link Document} for which the spaces per tab is requested
	 *
	 * @return the spaces per tab configured by the formatter which is activated
	 *         for the given {@link Document}, or {@code null} if the internal
	 *         NetBeans code formatter is used for the given {@code document}
	 */
	@CheckForNull
	public Integer getSpacesPerTab(Document document) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				return formatterService.getSpacesPerTab(document);
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return null;
	}

	/**
	 * Returns the currently configured {@link FormatterService} implementation for
	 * the given {@code document}. If the internal NetBeans formatter is configured
	 * or the implementation of the configured {@link FormatterService} could not be
	 * found {@code null} will be returned.
	 *
	 * @param document the {@link Document} for which the {@link FormatterService} is requested
	 *
	 * @return the currently configured {@link FormatterService} implementation
	 *         for the given {@code document}. If the internal NetBeans formatter is configured
	 *         or the implementation of the configured {@link FormatterService} could
	 *         not be found {@code null} will be returned
	 *
	 * @throws Exception if an error occurs
	 */
	@CheckForNull
	private FormatterService getActiveFormatterService(Document document) throws Exception {
		MimeType mimeType = MimeType.getMimeType(document);
		if (mimeType != null) {
			Preferences prefs = Settings.getActivePreferences(document);
			String activeFormatterId = prefs.get(Settings.ENABLED_FORMATTER_PREFIX + mimeType.toString(), Settings.DEFAULT_FORMATTER);

			Collection<? extends FormatterService> formatterServices = Lookup.getDefault().lookupAll(FormatterService.class);
			for (FormatterService formatterService : formatterServices) {
				if (Objects.equals(activeFormatterId, formatterService.getId())) {
					return formatterService;
				}
			}
		}

		return null;
	}

	/**
	 * Returns the expand tab to spaces flag configured by the formatter which is
	 * activated for the given {@link Document}, or {@code null} if the internal
	 * NetBeans code formatter is used for the given {@code document}.
	 *
	 * @param document the {@link Document} for which the expand tab to spaces flag is requested
	 *
	 * @return the expand tab to spaces flag configured by the formatter which is
	 *         activated for the given {@link Document}, or {@code null} if the
	 *         internal NetBeans code formatter is used for the given
	 *         {@code document}
	 */
	@CheckForNull
	public Boolean isExpandTabToSpaces(Document document) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				return formatterService.isExpandTabToSpaces(document);
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return null;
	}

	@CheckForNull
	public Boolean organizeImports(StyledDocument document, boolean afterFixImports) {
		try {
			FormatterService formatterService = getActiveFormatterService(document);
			if (formatterService != null && formatterService.canHandle(document)) {
				return formatterService.organizeImports(document, afterFixImports);
			}
		} catch (Exception e) {
			Exceptions.printStackTrace(e);
		}

		return null;
	}
}