001/*
002 * (C) Copyright 2014-2018 Nuxeo (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 *     Nuxeo
018 */
019
020package org.nuxeo.elasticsearch.config;
021
022import static org.nuxeo.elasticsearch.ElasticSearchConstants.ALL_FIELDS;
023import static org.nuxeo.elasticsearch.ElasticSearchConstants.BINARYTEXT_FIELD;
024import static org.nuxeo.elasticsearch.ElasticSearchConstants.DOC_TYPE;
025
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileNotFoundException;
029import java.io.IOException;
030import java.io.InputStream;
031
032import org.apache.commons.io.IOUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.nuxeo.common.Environment;
035import org.nuxeo.common.xmap.annotation.XNode;
036import org.nuxeo.common.xmap.annotation.XNodeList;
037import org.nuxeo.common.xmap.annotation.XObject;
038import org.nuxeo.elasticsearch.core.IncrementalIndexNameGenerator;
039
040/**
041 * XMap descriptor for configuring an index
042 *
043 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
044 */
045@XObject(value = "elasticSearchIndex")
046public class ElasticSearchIndexConfig {
047    public static final String DEFAULT_SETTING_FILE = "default-doc-settings.json";
048
049    public static final String DEFAULT_MAPPING_FILE = "default-doc-mapping.json";
050
051    protected static final String DEFAULT_REPOSITORY_NAME = "default";
052
053    protected static final String WRITE_SUFFIX = "-write";
054
055    @XNode("@enabled")
056    protected boolean isEnabled = true;
057
058    @XNode("@name")
059    protected String name;
060
061    // @since 9.3
062    @XNode("@manageAlias")
063    protected boolean manageAlias;
064
065    // @since 9.3
066    @XNode("@writeAlias")
067    protected String writeAlias;
068
069    @XNode("@repository")
070    protected String repositoryName;
071
072    @XNode("@type")
073    protected String type = DOC_TYPE;
074
075    @XNode("@create")
076    protected boolean create = true;
077
078    @XNode("settings")
079    protected String settings;
080
081    @XNode("settings@file")
082    protected String settingsFile;
083
084    @XNode("mapping")
085    protected String mapping;
086
087    @XNode("mapping@file")
088    protected String mappingFile;
089
090    @XNodeList(value = "fetchFromSource/exclude", type = String[].class, componentType = String.class)
091    protected String[] excludes;
092
093    @XNodeList(value = "fetchFromSource/include", type = String[].class, componentType = String.class)
094    protected String[] includes;
095
096    @Override
097    public String toString() {
098        if (isEnabled()) {
099            return String.format("EsIndexConfig(%s, %s, %s)", getName(), getRepositoryName(), getType());
100        }
101        return "EsIndexConfig disabled";
102    }
103
104    public String[] getExcludes() {
105        if (excludes == null) {
106            return new String[] { BINARYTEXT_FIELD };
107        }
108        return excludes;
109    }
110
111    public String[] getIncludes() {
112        if (includes == null || includes.length == 0) {
113            return new String[] { ALL_FIELDS };
114        }
115        return includes;
116    }
117
118    public String getName() {
119        return name;
120    }
121
122    public String getType() {
123        return type;
124    }
125
126    public String getSettings() {
127        if (settingsFile != null) {
128            return contentOfFile(settingsFile);
129        } else if (settings != null && !settings.isEmpty()) {
130            return settings;
131        }
132        return contentOfFile(DEFAULT_SETTING_FILE);
133    }
134
135    protected String contentOfFile(String filename) {
136        try (InputStream stream = getResourceStream(filename)) {
137            return IOUtils.toString(stream, "UTF-8");
138        } catch (IOException e) {
139            throw new IllegalArgumentException("Cannot load resource file: " + filename, e);
140        }
141    }
142
143    @SuppressWarnings("resource") // closed by caller
144    protected InputStream getResourceStream(String filename) {
145        // First check if the resource is available on the config directory
146        File file = new File(Environment.getDefault().getConfig(), filename);
147        if (file.exists()) {
148            try {
149                return new FileInputStream(file);
150            } catch (FileNotFoundException e) {
151                // try another way
152            }
153        }
154
155        // getResourceAsStream is needed getResource will not work when called from another module
156        InputStream ret = this.getClass().getClassLoader().getResourceAsStream(filename);
157        if (ret == null) {
158            // Then try to get it from jar
159            ret = this.getClass().getClassLoader().getResourceAsStream(filename);
160        }
161        if (ret == null) {
162            throw new IllegalArgumentException(
163                    String.format("Resource file cannot be found: %s or %s", file.getAbsolutePath(), filename));
164        }
165        return ret;
166    }
167
168    public String getMapping() {
169        if (mappingFile != null) {
170            return contentOfFile(mappingFile);
171        } else if (mapping != null && !mapping.isEmpty()) {
172            return mapping;
173        }
174        return contentOfFile(DEFAULT_MAPPING_FILE);
175    }
176
177    public boolean mustCreate() {
178        return create;
179    }
180
181    public String getRepositoryName() {
182        if (isDocumentIndex() && repositoryName == null) {
183            repositoryName = DEFAULT_REPOSITORY_NAME;
184        }
185        return repositoryName;
186    }
187
188    public boolean isEnabled() {
189        return isEnabled;
190    }
191
192    public void setEnabled(boolean isEnabled) {
193        this.isEnabled = isEnabled;
194    }
195
196    /**
197     * Return true if the index/mapping is associated with a Nuxeo document repository
198     *
199     * @since 7.4
200     */
201    public boolean isDocumentIndex() {
202        return DOC_TYPE.equals(getType());
203    }
204
205    /**
206     * Use {@code other} mapping and settings if not defined.
207     */
208    public void merge(final ElasticSearchIndexConfig other) {
209        if (other == null) {
210            return;
211        }
212        if (mapping == null && other.mapping != null) {
213            mapping = other.mapping;
214        }
215        if (settings == null && other.settings != null) {
216            settings = other.settings;
217        }
218        if (mappingFile == null && other.mappingFile != null) {
219            mappingFile = other.mappingFile;
220        }
221        if (settingsFile == null && other.settingsFile != null) {
222            settingsFile = other.settingsFile;
223        }
224    }
225
226    // @since 9.3
227    public boolean hasExplicitWriteIndex() {
228        return StringUtils.isNotBlank(writeAlias);
229    }
230
231    // @since 9.3
232    public String writeIndexOrAlias() {
233        // Custom alias managed outside of Nuxeo
234        if (hasExplicitWriteIndex()) {
235            return writeAlias;
236        }
237        // Nuxeo manages the write alias
238        if (manageAlias) {
239            return name + WRITE_SUFFIX;
240        }
241        // Simple index
242        return name;
243    }
244
245    // @since 9.3
246    public boolean manageAlias() {
247        return manageAlias;
248    }
249
250    // @since 9.3
251    public String newWriteIndexForAlias(String aliasName, String oldIndexName) {
252        // TODO make the alias resolver configurable
253        return new IncrementalIndexNameGenerator().getNextIndexName(aliasName, oldIndexName);
254    }
255}