/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.controller.services.path; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PATH; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.logging.ControllerLogger; import org.jboss.as.controller.registry.Resource; import org.jboss.as.controller.services.path.PathEntry.PathResolver; import org.jboss.as.controller.services.path.PathManager.Callback.Handle; import org.jboss.msc.service.Service; import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceName; import org.jboss.msc.service.ServiceTarget; import org.jboss.msc.service.StartContext; import org.jboss.msc.service.StartException; import org.jboss.msc.service.StopContext; /** * {@code PathManager} implementation that exposes additional methods used by the management operation handlers used * for paths, and also exposes the the {@code PathManager} as an MSC {@code Service}. * * @author <a href="kabir.khan@jboss.com">Kabir Khan</a> */ public abstract class PathManagerService implements PathManager, Service<PathManager> { public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("path", "manager"); //@GuardedBy(pathEntries) private final Map<String, PathEntry> pathEntries = new HashMap<String, PathEntry>(); //@GuardedBy(pathEntries) private final Map<String, Set<String>> dependenctRelativePaths = new HashMap<String, Set<String>>(); //@GuardedBy(callbacks) private final Map<String, Map<Event, Set<Callback>>> callbacks = new HashMap<String, Map<Event, Set<Callback>>>(); protected PathManagerService() { } /** * Add child resources to the given resource, one for each {@link PathEntry} currently associated with this * path manager. Used to initialize the model with resources for the standard paths that are not part of * the persistent configuration. * @param resource the resource to which children should be added. */ public final void addPathManagerResources(Resource resource) { synchronized (pathEntries) { for (PathEntry pathEntry : pathEntries.values()) { resource.registerChild(PathElement.pathElement(PATH, pathEntry.getName()), new HardcodedPathResource(PATH, pathEntry)); } } } @Override public final String resolveRelativePathEntry(String path, String relativeTo) { if (relativeTo == null) { return AbsolutePathService.convertPath(path); } else { PathEntry pathEntry; synchronized (pathEntries) { pathEntry = pathEntries.get(relativeTo); if (pathEntry == null) { throw ControllerLogger.ROOT_LOGGER.pathEntryNotFound(relativeTo); } return RelativePathService.doResolve(pathEntry.resolvePath(), path); } } } @Override public final Handle registerCallback(String name, Callback callback, Event...events) { synchronized (callbacks) { Map<Event, Set<Callback>> callbacksByEvent = callbacks.get(name); if (callbacksByEvent == null) { callbacksByEvent = new HashMap<PathManager.Event, Set<Callback>>(); callbacks.put(name, callbacksByEvent); } for (Event event : events) { Set<Callback> callbackSet = callbacksByEvent.get(event); if (callbackSet == null) { callbackSet = new HashSet<PathManager.Callback>(); callbacksByEvent.put(event, callbackSet); } callbackSet.add(callback); } } return new HandleImpl(name, callback, events); } @Override public final void start(StartContext context) throws StartException { } @Override public final void stop(StopContext context) { } @Override public final PathManagerService getValue() throws IllegalStateException, IllegalArgumentException { return this; } /** * Add a {@code PathEntry} and install a {@code Service<String>} for one of the standard read-only paths * that are determined from this process' environment. Not to be used for paths stored in the persistent * configuration. * @param serviceTarget service target to use for the service installation * @param pathName the logical name of the path within the model. Cannot be {@code null} * @param path the value of the path within the model. This is an absolute path. Cannot be {@code null} * @return the controller for the installed {@code Service<String>} */ protected final ServiceController<?> addHardcodedAbsolutePath(final ServiceTarget serviceTarget, final String pathName, final String path) { ServiceController<?> controller = addAbsolutePathService(serviceTarget, pathName, path); addPathEntry(pathName, path, null, true); return controller; } @Override public final PathEntry getPathEntry(String pathName) { synchronized (pathEntries) { PathEntry pathEntry = pathEntries.get(pathName); if (pathEntry == null) { throw ControllerLogger.ROOT_LOGGER.pathEntryNotFound(pathName); } return pathEntry; } } final void changePathServices(final OperationContext operationContext, String pathName, String path) { PathEntry pathEntry = findPathEntry(pathName); removePathService(operationContext, pathName); if (pathEntry.getRelativeTo() == null) { addAbsolutePathService(operationContext.getServiceTarget(), pathEntry.getName(), path); } else { addRelativePathService(operationContext.getServiceTarget(), pathEntry.getName(), path, false, pathEntry.getRelativeTo()); } } final void changeRelativePathServices(final OperationContext operationContext, String pathName, String relativeTo) { PathEntry pathEntry = findPathEntry(pathName); removePathService(operationContext, pathEntry.getName()); if (relativeTo == null) { addAbsolutePathService(operationContext.getServiceTarget(), pathEntry.getName(), pathEntry.getPath()); } else { addRelativePathService(operationContext.getServiceTarget(), pathEntry.getName(), pathEntry.getPath(), false, relativeTo); } } /** * Removes any {@code Service<String>} for the given path. * @param operationContext the operation context associated with the management operation making this request. Cannot be {@code null} * @param pathName the name of the relevant path. Cannot be {@code null} */ final void removePathService(final OperationContext operationContext, final String pathName) { final ServiceController<?> serviceController = operationContext.getServiceRegistry(true).getService(AbstractPathService.pathNameOf(pathName)); if (serviceController != null) { operationContext.removeService(serviceController); } } /** * Removes the entry for a path and sends an {@link org.jboss.as.controller.services.path.PathManager.Event#REMOVED} * notification to any registered * {@linkplain org.jboss.as.controller.services.path.PathManager.Callback#pathEvent(Event, PathEntry) callbacks}. * @param pathName the logical name of the path within the model. Cannot be {@code null} * @param check {@code true} if a check for the existence of other paths that depend on {@code pathName} * as their {@link PathEntry#getRelativeTo() relative-to} value should be performed * @throws OperationFailedException if {@code check} is {@code true} and other paths depend on the path being removed */ final void removePathEntry(final String pathName, boolean check) throws OperationFailedException{ synchronized (pathEntries) { PathEntry pathEntry = pathEntries.get(pathName); if (pathEntry.isReadOnly()) { throw ControllerLogger.ROOT_LOGGER.pathEntryIsReadOnly(pathName); } Set<String> dependents = dependenctRelativePaths.get(pathName); if (check && dependents != null) { throw ControllerLogger.ROOT_LOGGER.cannotRemovePathWithDependencies(pathName, dependents); } pathEntries.remove(pathName); triggerCallbacksForEvent(pathEntry, Event.REMOVED); if (pathEntry.getRelativeTo() != null) { dependents = dependenctRelativePaths.get(pathEntry.getRelativeTo()); if (dependents != null) { dependents.remove(pathEntry.getName()); if (dependents.size() == 0) { dependenctRelativePaths.remove(pathEntry.getRelativeTo()); } } } } } /** * Install a {@code Service<String>} for the given path. * @param serviceTarget the service target associated with the management operation making this request. Cannot be {@code null} * @param pathName the name of the relevant path. Cannot be {@code null} * @param path the value of the path within the model. This is either an absolute path or * the relative portion of the path. Cannot be {@code null} * * @return the service controller for the {@code Service<String>} */ final ServiceController<?> addAbsolutePathService(final ServiceTarget serviceTarget, final String pathName, final String path) { return AbsolutePathService.addService(pathName, path, serviceTarget, null); } /** * Install an {@code Service<String>} for the given path. * @param serviceTarget the service target associated with the management operation making this request. Cannot be {@code null} * @param pathName the name of the relevant path. Cannot be {@code null} * @param path the value of the path within the model. This is either an absolute path or * the relative portion of the path. Cannot be {@code null} * @param possiblyAbsolute {@code true} if the path may be absolute and a check should be performed before installing * a service variant that depends on the service associated with {@code relativeTo} * @param relativeTo the name of the path this path is relative to. If {@code null} this is an absolute path * @return the service controller for the {@code Service<String>} */ final ServiceController<?> addRelativePathService(final ServiceTarget serviceTarget, final String pathName, final String path, final boolean possiblyAbsolute, final String relativeTo) { if (possiblyAbsolute && AbstractPathService.isAbsoluteUnixOrWindowsPath(path)) { return addAbsolutePathService(serviceTarget, pathName, path); } else { return RelativePathService.addService(AbstractPathService.pathNameOf(pathName), path, possiblyAbsolute, relativeTo, serviceTarget, null); } } /** * Adds an entry for a path and sends an {@link org.jboss.as.controller.services.path.PathManager.Event#ADDED} * notification to any registered {@linkplain org.jboss.as.controller.services.path.PathManager.Callback callbacks}. * * @param pathName the logical name of the path within the model. Cannot be {@code null} * @param path the value of the path within the model. This is either an absolute path or * the relative portion of the path. Cannot be {@code null} * @param relativeTo the name of the path this path is relative to. If {@code null} this is an absolute path * @param readOnly {@code true} if the path is immutable, and cannot be removed or modified via a management operation * @return the entry that represents the path * * @throws RuntimeException if an entry with the given {@code pathName} is already registered */ final PathEntry addPathEntry(final String pathName, final String path, final String relativeTo, final boolean readOnly) { PathEntry pathEntry; synchronized (pathEntries) { if (pathEntries.containsKey(pathName)) { throw ControllerLogger.ROOT_LOGGER.pathEntryAlreadyExists(pathName); } pathEntry = new PathEntry(pathName, path, relativeTo, readOnly, relativeTo == null ? absoluteResolver : relativeResolver); pathEntries.put(pathName, pathEntry); if (relativeTo != null) { addDependent(pathName, relativeTo); } } triggerCallbacksForEvent(pathEntry, Event.ADDED); return pathEntry; } /** * Updates the {@link PathEntry#getRelativeTo() relative to} value for an entry and sends an * {@link org.jboss.as.controller.services.path.PathManager.Event#UPDATED} * notification to any registered * {@linkplain org.jboss.as.controller.services.path.PathManager.Callback#pathEvent(Event, PathEntry) callbacks}. * @param pathName the logical name of the path within the model. Cannot be {@code null} * @param relativePath the new name of the path this path is relative to. If {@code null} this is an absolute path * @param check {@code true} if a check for the existence of an entry for the new {@code relativePath} value * should be performed * @throws OperationFailedException if {@code check} is {@code true} and no path exists whose name matches {@code relativePath} */ final void changeRelativePath(String pathName, String relativePath, boolean check) throws OperationFailedException { PathEntry pathEntry = findPathEntry(pathName); synchronized (pathEntries) { if (check && relativePath != null && pathEntries.get(relativePath) == null) { // TODO per method signature and usage in PathWriteAttributeHandler this should throw OFE. // But leave it for now as a better way to deal with this is to have capabilities for paths // and use capability resolution to detect invalid references throw ControllerLogger.ROOT_LOGGER.pathEntryNotFound(pathName); } if (pathEntry.getRelativeTo() != null) { Set<String> dependents = dependenctRelativePaths.get(pathEntry.getRelativeTo()); dependents.remove(pathEntry.getName()); } pathEntry.setRelativeTo(relativePath); pathEntry.setPathResolver(relativePath == null ? absoluteResolver : relativeResolver); addDependent(pathEntry.getName(), pathEntry.getRelativeTo()); } triggerCallbacksForEvent(pathEntry, Event.UPDATED); } /** * Updates the {@link PathEntry#getPath() path} value for an entry and sends an * {@link org.jboss.as.controller.services.path.PathManager.Event#UPDATED} * notification to any registered * {@linkplain org.jboss.as.controller.services.path.PathManager.Callback#pathEvent(Event, PathEntry) callbacks}. * @param pathName the logical name of the path within the model. Cannot be {@code null} * @param path the value of the path within the model. This is either an absolute path or * the relative portion of the path. Cannot be {@code null} */ final void changePath(String pathName, String path) { PathEntry pathEntry = findPathEntry(pathName); pathEntry.setPath(path); triggerCallbacksForEvent(pathEntry, Event.UPDATED); } //Must be called with pathEntries lock taken private void addDependent(String pathName, String relativeTo) { if (relativeTo != null) { Set<String> dependents = dependenctRelativePaths.get(relativeTo); if (dependents == null) { dependents = new HashSet<String>(); dependenctRelativePaths.put(relativeTo, dependents); } dependents.add(pathName); } } private PathEntry findPathEntry(String pathName) { PathEntry pathEntry; synchronized (pathEntries) { pathEntry = pathEntries.get(pathName); if (pathEntry == null) { throw ControllerLogger.ROOT_LOGGER.pathEntryNotFound(pathName); } } return pathEntry; } private void triggerCallbacksForEvent(PathEntry pathEntry, Event event) { Set<PathEntry> allEntries = null; synchronized (pathEntries) { if (event == Event.UPDATED) { allEntries = new LinkedHashSet<PathEntry>(); allEntries.add(pathEntry); getAllDependents(allEntries, pathEntry.getName()); } else { allEntries = Collections.singleton(pathEntry); } } Map<PathEntry, Set<Callback>> triggerCallbacks = new LinkedHashMap<PathEntry, Set<Callback>>(); synchronized (callbacks) { for (PathEntry pe : allEntries) { Map<Event, Set<Callback>> callbacksByEvent = callbacks.get(pe.getName()); if (callbacksByEvent != null) { Set<Callback> callbacksForEntry = callbacksByEvent.get(event); if (callbacksForEntry != null) { triggerCallbacks.put(pe, new LinkedHashSet<Callback>(callbacksForEntry)); } } } } for (Map.Entry<PathEntry, Set<Callback>> entry : triggerCallbacks.entrySet()) { for (Callback cb : entry.getValue()) { cb.pathEvent(event, entry.getKey()); } } } /** * Creates a {@link org.jboss.as.controller.services.path.PathManager.PathEventContext} and passes it to the * {@link org.jboss.as.controller.services.path.PathManager.Callback#pathModelEvent(PathEventContext, String)} method * of any callbacks to allow the to record if the event should trigger a restart or reload required state. The caller * can then use the {@link PathEventContextImpl#isInstallServices()} method to check if further updates to * the path manager should be made. * @param operationContext the operation context associated with the management operation making this request * @param name the name of the relevant path. Cannot be {@code null} * @param event the event. Cannot be {@code null} * @return the path event context to use to check if further updates should be made */ PathEventContextImpl checkRestartRequired(OperationContext operationContext, String name, Event event) { Set<String> allEntries = null; synchronized (pathEntries) { if (event == Event.UPDATED) { allEntries = new LinkedHashSet<String>(); allEntries.add(name); getAllDependentsForRestartCheck(allEntries, name); } else { allEntries = Collections.singleton(name); } } Map<String, Set<Callback>> triggerCallbacks = new LinkedHashMap<String, Set<Callback>>(); synchronized (callbacks) { for (String pathName : allEntries) { Map<Event, Set<Callback>> callbacksByEvent = callbacks.get(pathName); if (callbacksByEvent != null) { Set<Callback> callbacksForEntry = callbacksByEvent.get(event); if (callbacksForEntry != null) { triggerCallbacks.put(pathName, new LinkedHashSet<Callback>(callbacksForEntry)); } } } } PathEventContextImpl pathEventContext = new PathEventContextImpl(operationContext, event); for (Map.Entry<String, Set<Callback>> entry : triggerCallbacks.entrySet()) { for (Callback cb : entry.getValue()) { cb.pathModelEvent(pathEventContext, entry.getKey()); if (pathEventContext.restart) { return pathEventContext; } } } return pathEventContext; } //Call with pathEntries lock taken private void getAllDependents(Set<PathEntry> result, String name) { Set<String> depNames = dependenctRelativePaths.get(name); if (depNames == null) { return; } for (String dep : depNames) { PathEntry entry = pathEntries.get(dep); if (entry != null) { result.add(entry); getAllDependents(result, dep); } } } //Call with pathEntries lock taken private void getAllDependentsForRestartCheck(Set<String> result, String name) { Set<String> depNames = dependenctRelativePaths.get(name); if (depNames == null) { return; } for (String dep : depNames) { PathEntry entry = pathEntries.get(dep); if (entry != null) { result.add(dep); getAllDependentsForRestartCheck(result, dep); } } } private final PathResolver absoluteResolver = new PathResolver() { @Override public String resolvePath(String name, String path, String relativeTo, PathResolver resolver) { return AbsolutePathService.convertPath(path); } @Override public boolean isResolved(String relativeTo) { return true; } }; private final PathResolver relativeResolver = new PathResolver() { @Override public String resolvePath(String name, String path, String relativeTo, PathResolver resolver) { PathEntry relativeEntry; synchronized (pathEntries) { relativeEntry = pathEntries.get(relativeTo); if (relativeEntry == null) { throw new IllegalStateException("Could not find relativeTo path '" + relativeTo + "' for relative path '" + name); } } return RelativePathService.doResolve(relativeEntry.resolvePath(), path); } @Override public boolean isResolved(String relativeTo) { synchronized (pathEntries) { return pathEntries.containsKey(relativeTo); } } }; private class HandleImpl implements Handle { private final String pathName; private final Callback callback; private final Event[] events; public HandleImpl(String pathName, Callback callback, Event...events) { this.pathName = pathName; this.callback = callback; this.events = events; } public void remove() { synchronized (callbacks) { Map<Event, Set<Callback>> callbacksByEvent = callbacks.get(pathName); if (callbacksByEvent != null) { for (Event event : events) { Set<Callback> callbackSet = callbacksByEvent.get(event); if (callbackSet != null) { callbackSet.remove(callback); } if (callbackSet.isEmpty()) { callbacksByEvent.remove(event); } } if (callbacksByEvent.isEmpty()) { callbacks.remove(pathName); } } } } } static class PathEventContextImpl implements PathEventContext { private final OperationContext operationContext; private final Event event; private volatile boolean reload; private volatile boolean restart; PathEventContextImpl(OperationContext operationContext, Event event) { this.operationContext = operationContext; this.event = event; } public boolean isBooting() { return operationContext.isBooting(); } public boolean isNormalServer() { return operationContext.isNormalServer(); } public boolean isResourceServiceRestartAllowed() { return operationContext.isResourceServiceRestartAllowed(); } public void reloadRequired() { reload = true; operationContext.reloadRequired(); } public void restartRequired() { restart = true; operationContext.restartRequired(); } @Override public Event getEvent() { return event; } void revert() { if (restart) { operationContext.revertRestartRequired(); } if (reload) { operationContext.revertReloadRequired(); } } boolean isInstallServices() { return !restart && !reload; } } }