001/*
002 * (C) Copyright 2012-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Anahide Tchertchian
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.directory;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034/**
035 * Generic {@link BaseDirectoryDescriptor} registry holding registered descriptors and instantiated {@link Directory}
036 * objects.
037 * <p>
038 * The directory descriptors have two special boolean flags that control how merge works:
039 * <ul>
040 * <li>{@code remove="true"}: this removes the definition of the directory. The next definition (if any) will be done
041 * from scratch.
042 * <li>{@code template="true"}: this defines an abstract descriptor which cannot be directly instantiated as a
043 * directory. However another descriptor can extend it through {@code extends="templatename"} to inherit all its
044 * properties.
045 * </ul>
046 *
047 * @since 8.2
048 */
049public class DirectoryRegistry {
050
051    private static final Log log = LogFactory.getLog(DirectoryRegistry.class);
052
053    /** All descriptors registered. */
054    // used under synchronization
055    protected Map<String, List<BaseDirectoryDescriptor>> allDescriptors = new HashMap<>();
056
057    /** Effective descriptors. */
058    // used under synchronization
059    protected Map<String, BaseDirectoryDescriptor> descriptors = new HashMap<>();
060
061    /** Effective instantiated directories. */
062    // used under synchronization
063    protected Map<String, Directory> directories = new HashMap<>();
064
065    public synchronized void addContribution(BaseDirectoryDescriptor contrib) {
066        String id = contrib.name;
067        if (id.contains("/") && log.isWarnEnabled()) {
068            log.warn("Directory " + id + " should not contain forward slashes in its name, as they are not supported."
069                    + " Operations with the REST API on this directory won't work.");
070        }
071        log.info("Registered directory" + (contrib.template ? " template" : "") + ": " + id);
072        allDescriptors.computeIfAbsent(id, k -> new ArrayList<>()).add(contrib);
073        contributionChanged(contrib);
074    }
075
076    public synchronized void removeContribution(BaseDirectoryDescriptor contrib) {
077        String id = contrib.name;
078        log.info("Unregistered directory" + (contrib.template ? " template" : "") + ": " + id);
079        allDescriptors.getOrDefault(id, Collections.emptyList()).remove(contrib);
080        contributionChanged(contrib);
081    }
082
083    protected void contributionChanged(BaseDirectoryDescriptor contrib) {
084        LinkedList<String> todo = new LinkedList<>();
085        todo.add(contrib.name);
086        Set<String> done = new HashSet<>();
087        while (!todo.isEmpty()) {
088            String id = todo.removeFirst();
089            if (!done.add(id)) {
090                // already done, avoid loops
091                continue;
092            }
093            BaseDirectoryDescriptor desc = recomputeDescriptor(id);
094            // recompute dependencies
095            if (desc != null) {
096                for (List<BaseDirectoryDescriptor> list : allDescriptors.values()) {
097                    for (BaseDirectoryDescriptor d : list) {
098                        if (id.equals(d.extendz)) {
099                            todo.add(d.name);
100                            break;
101                        }
102                    }
103                }
104            }
105        }
106    }
107
108    protected void removeDirectory(String id) {
109        Directory dir = directories.remove(id);
110        if (dir != null) {
111            shutdownDirectory(dir);
112        }
113    }
114
115    /** Recomputes the effective descriptor for a directory id. */
116    protected BaseDirectoryDescriptor recomputeDescriptor(String id) {
117        removeDirectory(id);
118        // compute effective descriptor
119        List<BaseDirectoryDescriptor> list = allDescriptors.getOrDefault(id, Collections.emptyList());
120        BaseDirectoryDescriptor contrib = null;
121        for (BaseDirectoryDescriptor next : list) {
122            String extendz = next.extendz;
123            if (extendz != null) {
124                // merge from base
125                BaseDirectoryDescriptor base = descriptors.get(extendz);
126                if (base != null && base.template) {
127                    // merge generic base descriptor into specific one from the template
128                    contrib = base.clone();
129                    contrib.template = false;
130                    contrib.name = next.name;
131                    contrib.merge(next);
132                } else {
133                    log.debug("Directory " + id + " extends non-existing directory template: " + extendz);
134                    contrib = null;
135                }
136            } else if (next.remove) {
137                contrib = null;
138            } else if (contrib == null) {
139                // first descriptor or first one after a remove
140                contrib = next.clone();
141            } else if (contrib.getClass() == next.getClass()) {
142                contrib.merge(next);
143            } else {
144                log.warn("Directory " + id + " redefined with different factory");
145                contrib = next.clone();
146            }
147        }
148        if (contrib == null) {
149            descriptors.remove(id);
150        } else {
151            descriptors.put(id, contrib);
152        }
153        return contrib;
154    }
155
156    /**
157     * Gets the effective directory descriptor with the given id.
158     * <p>
159     * Templates are not returned.
160     *
161     * @param id the directory id
162     * @return the effective directory descriptor, or {@code null} if not found
163     */
164    public synchronized BaseDirectoryDescriptor getDirectoryDescriptor(String id) {
165        BaseDirectoryDescriptor descriptor = descriptors.get(id);
166        return descriptor.template ? null : descriptor;
167    }
168
169    /**
170     * Gets the directory with the given id.
171     *
172     * @param id the directory id
173     * @return the directory, or {@code null} if not found
174     */
175    public synchronized Directory getDirectory(String id) {
176        Directory dir = directories.get(id);
177        if (dir == null) {
178            BaseDirectoryDescriptor descriptor = descriptors.get(id);
179            if (descriptor != null && !descriptor.template) {
180                dir = descriptor.newDirectory();
181                directories.put(id, dir);
182            }
183        }
184        return dir;
185    }
186
187    /**
188     * Gets all the directory ids.
189     *
190     * @return the directory ids
191     */
192    public synchronized List<String> getDirectoryIds() {
193        List<String> list = new ArrayList<>();
194        for (BaseDirectoryDescriptor descriptor : descriptors.values()) {
195            if (descriptor.template) {
196                continue;
197            }
198            list.add(descriptor.name);
199        }
200        return list;
201    }
202
203    /**
204     * Gets all the directories.
205     *
206     * @return the directories
207     */
208    public synchronized List<Directory> getDirectories() {
209        List<Directory> list = new ArrayList<>();
210        for (BaseDirectoryDescriptor descriptor : descriptors.values()) {
211            if (descriptor.template) {
212                continue;
213            }
214            list.add(getDirectory(descriptor.name));
215        }
216        return list;
217    }
218
219    /**
220     * Shuts down all directories and clears the registry.
221     */
222    public synchronized void shutdown() {
223        for (Directory dir : directories.values()) {
224            shutdownDirectory(dir);
225        }
226        allDescriptors.clear();
227        descriptors.clear();
228        directories.clear();
229    }
230
231    /**
232     * Shuts down the given directory and catches any {@link DirectoryException}.
233     *
234     * @param dir the directory
235     */
236    protected static void shutdownDirectory(Directory dir) {
237        try {
238            dir.shutdown();
239        } catch (DirectoryException e) {
240            log.error("Error while shutting down directory:" + dir.getName(), e);
241        }
242    }
243
244}