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}