001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.dataAccess;
019
020
021import io.stallion.dataAccess.db.*;
022import io.stallion.dataAccess.file.*;
023import io.stallion.exceptions.ConfigException;
024import io.stallion.exceptions.UsageException;
025import io.stallion.services.Log;
026import io.stallion.settings.Settings;
027import io.stallion.settings.ContentFolder;
028import org.apache.commons.lang3.StringUtils;
029
030import javax.persistence.Table;
031import java.io.File;
032import java.util.*;
033
034
035import static io.stallion.utils.Literals.*;
036import static io.stallion.Context.*;
037
038/**
039 * The DalRegistry, or Data Access Layer Registry, allows for ModelControllers to be
040 * registered, booted up, and then accessed.
041 *
042 */
043public class DataAccessRegistry implements Map<String, ModelController>  {
044    private Map<String, ModelController> internalMap = new HashMap<String, ModelController>();
045    private Map<String, String> modelClassToBucketName = new HashMap<>();
046    private DB db;
047    private Tickets tickets;
048
049    private TextItemController<TextItem> pages;
050    private TextItemController<TextItem> posts;
051
052    private Set<String> deduping = new HashSet<String>();
053
054    private static DataAccessRegistry _instance;
055
056    public static DataAccessRegistry instance() {
057        if (_instance == null) {
058            throw new UsageException("You must call load() before accessing the DalRegistry.");
059        }
060        return _instance;
061    }
062
063    public static DataAccessRegistry load() {
064        _instance = new DataAccessRegistry();
065        try {
066            _instance.loadAndHydrate();
067        } catch (Exception e) {
068            throw new RuntimeException(e);
069        }
070        return _instance;
071    }
072
073    public void deregister(String bucket) {
074        internalMap.remove(bucket);
075        deduping.remove(bucket);
076    }
077
078    public static void shutdown() {
079        _instance = null;
080    }
081
082    public DataAccessRegistry() {
083        if (DB.instance() != null && DB.instance().getTickets() != null) {
084            tickets = DB.instance().getTickets();
085        } else {
086            tickets = new TimebasedTickets();
087        }
088    }
089
090    public void loadAndHydrate() throws Exception {
091        loadAndHydrate(Settings.instance().getTargetFolder(), Settings.instance());
092    }
093
094    public void loadAndHydrate(String targetFolder, Settings settings) throws Exception {
095        loadFileBasedDataAccess(targetFolder, settings);
096        //loadDbBasedDal(settings);
097    }
098
099    /**
100     * Loads data access controllers for the pages folder, posts folder, and any folders defined
101     * in the settings [TargetFolder] block
102     * @param targetFolder
103     * @param settings
104     * @throws Exception
105     */
106    private void loadFileBasedDataAccess(String targetFolder, Settings settings) throws Exception {
107        List<DataAccessRegistration> registrations = new ArrayList<>();
108        List<ContentFolder> folders = new ArrayList<>();
109        if (settings.getFolders() != null) {
110            folders.addAll(settings.getFolders());
111        }
112
113        //List<String> names = new ArrayList<>();
114        //if (new File(targetFolder + "/pages").isDirectory()) {
115        //    folders.add(new ContentFolder().setPath(targetFolder + "/pages").setType("markdown").setItemTemplate(settings.getPageTemplate()));
116        //}
117
118        for (ContentFolder folder: folders) {
119            DataAccessRegistration registration = new DataAccessRegistration();
120            registration.setUseDataFolder(false);
121            registration.setPath(folder.getPath());
122            registration.setWritable(folder.getWritable());
123            if (!StringUtils.isBlank(folder.getType()) && folder.getType().equals("json")) {
124                registration.setControllerClass(StandardModelController.class);
125                registration.setModelClass(MappedModelBase.class);
126                registration.setPersisterClass(JsonFilePersister.class);
127            } else if (!StringUtils.isBlank(folder.getType()) && folder.getType().equals("toml")) {
128                registration.setControllerClass(TomlItemController.class);
129                registration.setModelClass(TomlItem.class);
130                registration.setPersisterClass(TomlPersister.class);
131                registration.setWritable(false);
132            } else {
133                registration.setControllerClass(TextItemController.class);
134                registration.setModelClass(TextItem.class);
135                registration.setPersisterClass(TextFilePersister.class);
136
137            }
138            registration.setTemplatePath(folder.getItemTemplate());
139            registration.setShouldWatch(true);
140            if (!StringUtils.isEmpty(folder.getClassName())) {
141                Class clazz = this.getClass().getClassLoader().loadClass(folder.getClassName());
142                registration.setModelClass(clazz);
143            }
144            register(registration);
145        }
146
147        if (containsKey("pages")) {
148            setPages((TextItemController) get("pages"));
149        }
150    }
151
152    public ModelController registerDbModel(Class<? extends Model> model, Class<? extends ModelController> controller) {
153        return registerDbModel(model, controller, LocalMemoryStash.class);
154    }
155
156    public ModelController registerDbModel(Class<? extends Model> model, Class<? extends ModelController> controller, boolean syncToMemory) {
157        Class<? extends Stash> cls = LocalMemoryStash.class;
158        if (!syncToMemory) {
159            cls = NoStash.class;
160        }
161        return registerDbModel(model, controller, cls);
162    }
163    public ModelController registerDbModel(Class<? extends Model> model, Class<? extends ModelController> controller, Class<? extends Stash> stash) {
164        return registerDbModel(model, controller, stash, null);
165    }
166    /**
167     * Registers the given model and controller with a database persister, getting the bucket name
168     * from the @Table annotation on the model.
169     *
170     * @param model
171     * @param controller
172     * @param stash
173     * @return
174     */
175    public ModelController registerDbModel(Class<? extends Model> model, Class<? extends ModelController> controller, Class<? extends Stash> stash, String bucket) {
176        Table anno = model.getAnnotation(Table.class);
177        if (anno == null) {
178            throw new UsageException("A @Table annotation is required on the model " + model.getCanonicalName() + " in order to register it.");
179        }
180        bucket = or(bucket, anno.name());
181        String table = anno.name();
182        DataAccessRegistration registration = new DataAccessRegistration()
183                .setDatabaseBacked(true)
184                .setPersisterClass(DbPersister.class)
185                .setBucket(bucket)
186                .setTableName(table)
187                .setControllerClass(controller)
188                .setStashClass(stash)
189                .setModelClass(model);
190        return register(registration);
191    }
192
193    /**
194     * Registers the data store defined by the passed in DalRegistration.  Does everything
195     * including instantiating the persister, controller, and stash; syncing all data
196     * into the stash if applicable; setting up file system watchers if applicable; and
197     * adding the controller to the internal registry so that it be accessed via
198     * DalRegistry.instance().get(bucketName) and so that it will be available in
199     * templates.
200     *
201     *
202     *
203     * @param registration
204     */
205    public ModelController register(DataAccessRegistration registration) {
206
207        // Validation, de-duping, and normalization
208        if (StringUtils.isEmpty(registration.getPath()) && StringUtils.isEmpty(registration.getTableName()) && empty(registration.getBucket())) {
209            throw new ConfigException(String.format("You tried to load a model/controller. But both the folder path and the table name were empty. One the two must be set. model=%s, controller=%s", registration.getModelClass(), registration.getControllerClass()));
210        }
211        registration.build(settings().getTargetFolder());
212        if (deduping.contains(registration.getBucket())) {
213            throw new ConfigException(String.format("Bucket was registered twice: %s", registration.getBucket()));
214        }
215        if (!StringUtils.isEmpty(registration.getAbsolutePath())) {
216            if (deduping.contains(registration.getAbsolutePath())) {
217                throw new ConfigException(String.format("registered same path twice: %s", registration.getAbsolutePath()));
218            }
219        }
220        if (internalMap.containsKey(registration.getBucket())) {
221            throw new ConfigException(String.format("Bucket controller was registered twice: %s", registration.getBucket()));
222        }
223        deduping.add(registration.getAbsolutePath());
224        deduping.add(registration.getBucket());
225
226
227
228
229        // Load the persister
230
231        Persister persister;
232        try {
233            persister = registration.getPersisterClass().newInstance();
234        } catch (Exception e) {
235            Log.warn("Could not instaniate persister instance for class {0} bucket {1}", registration.getPersisterClass(), registration.getRelativePath());
236            throw new RuntimeException(e);
237        }
238
239
240        /*
241        */
242
243        // Register the model with the database, for schema purposes
244        if (persister.isDbBacked()) {
245            if (DB.isUseDummyPersisterForSqlGenerationMode()) {
246                persister = new DummyPersister<>();
247            }
248            if (DB.instance() == null) {
249                    throw new ConfigException("You are using a model that requires a database, but you database configuration is empty.");
250            }
251            if (registration.getDynamicModelDefinition() != null) {
252                DB.instance().addDynamicModelDefinition(registration.getDynamicModelDefinition());
253            } else {
254                DB.instance().addModel(registration.getModelClass());
255            }
256        }  else if (registration.isWritable()) {
257            File folder = new File(registration.getAbsolutePath());
258            if (!folder.isDirectory()) {
259                folder.mkdirs();
260            }
261        }
262
263
264        // Register the stash
265        Stash stash;
266        try {
267            stash = registration.getStashClass().newInstance();
268        } catch (InstantiationException e) {
269            Log.warn("Could not instaniate stash instance for class {0} bucket {1}", registration.getStashClass(), registration.getBucket());
270            throw new RuntimeException(e);
271        } catch (IllegalAccessException e) {
272            Log.warn("Could not instaniate stash instance for class {0} bucket {1}", registration.getStashClass(), registration.getBucket());
273            throw new RuntimeException(e);
274
275        }
276
277        // Register the item controller
278        ModelController controller;
279        try {
280            controller = registration.getControllerClass().getDeclaredConstructor().newInstance();
281        } catch (Exception e) {
282            Log.warn("Could not instaniate controller instance for class {0} bucket {1}", registration.getControllerClass(), registration.getRelativePath());
283            throw new RuntimeException(e);
284        }
285
286
287
288        // Initialize all the things
289        controller.init(registration, persister, stash);
290        persister.init(registration, controller, stash);
291        stash.init(registration, controller, persister);
292
293
294        // Add the controller to the DalRegistry lookup table
295        internalMap.put(controller.getBucket(), controller);
296        // Add the model to the DalRegistry lookup table
297        Log.info("Register model {0}", registration.getModelClass().getCanonicalName());
298        modelClassToBucketName.put(registration.getModelClass().getCanonicalName(), registration.getBucket());
299
300        // Load all the items into the local stash
301        stash.loadAll();
302
303
304        // Attach any file system watchers
305        if (registration.isShouldWatch()) {
306            try {
307                persister.attachWatcher();
308            } catch (Exception e) {
309                Log.warn("Could not attach watcher for persister {0} bucket {1} absolute path {2}", registration.getPersisterClass(), registration.getRelativePath(), registration.getAbsolutePath());
310                throw new RuntimeException(e);
311            }
312        }
313        return controller;
314    }
315
316    @Deprecated
317    public ModelController getControllerForModelName(String modelName) {
318        if (containsKey(modelName)) {
319            return get(modelName);
320        }
321        String bucket = modelClassToBucketName.get(modelName);
322        return get(bucket);
323    }
324
325    @Deprecated
326    public ModelController getControllerForModel(Class<? extends Model> model) {
327        return getControllerForModelName(model.getCanonicalName());
328    }
329
330    public ModelController<? extends Model> get(String key) {
331        return internalMap.get(key);
332    }
333
334    public TextItemController<TextItem> getPages() {
335        return pages;
336    }
337
338    public void setPages(TextItemController<TextItem> pages) {
339        this.pages = pages;
340    }
341
342    public TextItemController<TextItem> getPosts() {
343        return posts;
344    }
345
346    public void setPosts(TextItemController<TextItem> posts) {
347        this.posts = posts;
348    }
349
350    @Override
351    public int size() {
352        return internalMap.size();
353    }
354
355    @Override
356    public boolean isEmpty() {
357        return internalMap.isEmpty();
358    }
359
360    @Override
361    public boolean containsKey(Object key) {
362        return internalMap.containsKey(key);
363    }
364
365    @Override
366    public boolean containsValue(Object value) {
367        return internalMap.containsValue(value);
368    }
369
370    @Override
371    public ModelController get(Object key) {
372        try {
373            return this.internalMap.get(key);
374        } catch(Exception e) {
375            throw new RuntimeException(e);
376        }
377    }
378
379    public ModelController getNamespaced(String nameSpace, Object key) {
380        try {
381            return this.internalMap.get(nameSpace + "-" + key);
382        } catch(Exception e) {
383            throw new RuntimeException(e);
384        }
385    }
386
387
388    @Override
389    public ModelController put(String key, ModelController value) {
390        throw new RuntimeException("You must use registerController() method to add an item controller");
391    }
392
393    @Override
394    public ModelController remove(Object key) {
395        return null;
396    }
397
398    @Override
399    public void putAll(Map<? extends String, ? extends ModelController> m) {
400
401    }
402
403    @Override
404    public void clear() {
405        throw new RuntimeException("Not implemented");
406    }
407
408    @Override
409    public Set<String> keySet() {
410        return internalMap.keySet();
411    }
412
413    @Override
414    public Collection<ModelController> values() {
415        throw new RuntimeException("Not implemented");
416    }
417
418    @Override
419    public Set<Entry<String, ModelController>> entrySet() {
420        return internalMap.entrySet();
421    }
422
423    public DB getDb() {
424        return db;
425    }
426
427    public void setDb(DB db) {
428        this.db = db;
429    }
430
431    public Tickets getTickets() {
432        return tickets;
433    }
434
435    public void setTickets(Tickets tickets) {
436        this.tickets = tickets;
437    }
438}