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<String>();
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     *
159     * @param id the directory id
160     * @return the effective directory descriptor, or {@code null} if not found
161     */
162    public synchronized BaseDirectoryDescriptor getDirectoryDescriptor(String id) {
163        return descriptors.get(id);
164    }
165
166    /**
167     * Gets the directory with the given id.
168     *
169     * @param id the directory id
170     * @return the directory, or {@code null} if not found
171     */
172    public synchronized Directory getDirectory(String id) {
173        Directory dir = directories.get(id);
174        if (dir == null) {
175            BaseDirectoryDescriptor descriptor = descriptors.get(id);
176            if (descriptor != null) {
177                dir = descriptor.newDirectory();
178                directories.put(id, dir);
179            }
180        }
181        return dir;
182    }
183
184    /**
185     * Gets all the directory ids.
186     *
187     * @return the directory ids
188     */
189    public synchronized List<String> getDirectoryIds() {
190        List<String> list = new ArrayList<>();
191        for (BaseDirectoryDescriptor descriptor : descriptors.values()) {
192            if (descriptor.template) {
193                continue;
194            }
195            list.add(descriptor.name);
196        }
197        return list;
198    }
199
200    /**
201     * Gets all the directories.
202     *
203     * @return the directories
204     */
205    public synchronized List<Directory> getDirectories() {
206        List<Directory> list = new ArrayList<>();
207        for (BaseDirectoryDescriptor descriptor : descriptors.values()) {
208            if (descriptor.template) {
209                continue;
210            }
211            list.add(getDirectory(descriptor.name));
212        }
213        return list;
214    }
215
216    /**
217     * Shuts down all directories and clears the registry.
218     */
219    public synchronized void shutdown() {
220        for (Directory dir : directories.values()) {
221            shutdownDirectory(dir);
222        }
223        allDescriptors.clear();
224        descriptors.clear();
225        directories.clear();
226    }
227
228    /**
229     * Shuts down the given directory and catches any {@link DirectoryException}.
230     *
231     * @param dir the directory
232     */
233    protected static void shutdownDirectory(Directory dir) {
234        try {
235            dir.shutdown();
236        } catch (DirectoryException e) {
237            log.error("Error while shutting down directory:" + dir.getName(), e);
238        }
239    }
240
241}