001/* 002 * PermissionsEx 003 * Copyright (C) zml and PermissionsEx contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ca.stellardrift.permissionsex.datastore.sql; 018 019import ca.stellardrift.permissionsex.impl.PermissionsEx; 020import ca.stellardrift.permissionsex.impl.config.FilePermissionsExConfiguration; 021import ca.stellardrift.permissionsex.datastore.DataStoreFactory; 022import ca.stellardrift.permissionsex.datastore.StoreProperties; 023import ca.stellardrift.permissionsex.datastore.sql.dao.H2SqlDao; 024import ca.stellardrift.permissionsex.datastore.sql.dao.MySqlDao; 025import ca.stellardrift.permissionsex.datastore.sql.dao.SchemaMigration; 026import ca.stellardrift.permissionsex.impl.backend.AbstractDataStore; 027import ca.stellardrift.permissionsex.datastore.DataStore; 028import ca.stellardrift.permissionsex.context.ContextValue; 029import ca.stellardrift.permissionsex.context.ContextInheritance; 030import ca.stellardrift.permissionsex.impl.util.PCollections; 031import ca.stellardrift.permissionsex.subject.ImmutableSubjectData; 032import ca.stellardrift.permissionsex.exception.PermissionsLoadingException; 033import ca.stellardrift.permissionsex.rank.RankLadder; 034import ca.stellardrift.permissionsex.subject.SubjectRef; 035import com.google.auto.service.AutoService; 036import org.checkerframework.checker.nullness.qual.Nullable; 037import org.pcollections.PMap; 038import org.pcollections.PSet; 039import org.spongepowered.configurate.BasicConfigurationNode; 040import org.spongepowered.configurate.objectmapping.ConfigSerializable; 041import org.spongepowered.configurate.objectmapping.meta.Setting; 042import org.spongepowered.configurate.util.CheckedFunction; 043import org.spongepowered.configurate.util.UnmodifiableCollections; 044 045import javax.sql.DataSource; 046import java.sql.Connection; 047import java.sql.SQLException; 048import java.util.Collections; 049import java.util.HashSet; 050import java.util.List; 051import java.util.Map; 052import java.util.Optional; 053import java.util.Set; 054import java.util.concurrent.CompletableFuture; 055import java.util.concurrent.ConcurrentHashMap; 056import java.util.concurrent.ConcurrentMap; 057import java.util.function.Function; 058import java.util.regex.Pattern; 059import java.util.stream.Stream; 060 061import static ca.stellardrift.permissionsex.datastore.sql.SchemaMigrations.VERSION_LATEST; 062 063/** 064 * DataSource for SQL data. 065 */ 066public final class SqlDataStore extends AbstractDataStore<SqlDataStore, SqlDataStore.Config> { 067 private static final Pattern BRACES_PATTERN = Pattern.compile("\\{\\}"); 068 private boolean autoInitialize = true; 069 070 SqlDataStore(final StoreProperties<Config> properties) { 071 super(properties); 072 } 073 074 PermissionsEx<?> manager() { 075 return (PermissionsEx<?>) this.getManager(); 076 } 077 078 @AutoService(DataStoreFactory.class) 079 public static final class Factory extends AbstractDataStore.Factory<SqlDataStore, Config> { 080 081 static String ID = "sql"; 082 083 public Factory() { 084 super(ID, Config.class, SqlDataStore::new); 085 } 086 } 087 088 @ConfigSerializable 089 static class Config { 090 @Setting("url") 091 private String connectionUrl; 092 @Setting("prefix") 093 private String prefix = "pex"; 094 private transient String realPrefix; 095 @Setting("aliases") 096 private Map<String, String> legacyAliases; 097 098 String prefix() { 099 if (this.realPrefix == null) { 100 if (this.prefix != null && !this.prefix.isEmpty() && !this.prefix.endsWith("_")) { 101 this.realPrefix = this.prefix + "_"; 102 } else if (this.prefix == null) { 103 this.realPrefix = ""; 104 } else { 105 this.realPrefix = this.prefix; 106 } 107 } 108 return this.realPrefix; 109 } 110 } 111 112 // For testing 113 static SqlDataStore create(final String ident) { 114 try { 115 return (SqlDataStore) DataStoreFactory.forType(Factory.ID).create(ident, BasicConfigurationNode.root(FilePermissionsExConfiguration.PEX_OPTIONS)); 116 } catch (PermissionsLoadingException e) { 117 throw new RuntimeException(e); 118 } 119 } 120 121 private final ConcurrentMap<String, String> queryPrefixCache = new ConcurrentHashMap<>(); 122 private final ThreadLocal<@Nullable SqlDao> heldDao = new ThreadLocal<>(); 123 private final PMap<String, CheckedFunction<SqlDataStore, SqlDao, SQLException>> daoImplementations = PCollections.<String, CheckedFunction<SqlDataStore, SqlDao, SQLException>>map("mysql", MySqlDao::new) 124 .plus("h2", H2SqlDao::new); 125 private CheckedFunction<SqlDataStore, SqlDao, SQLException> daoFactory; 126 private DataSource sql; 127 128 SqlDao getDao() throws SQLException { 129 final @Nullable SqlDao dao = this.heldDao.get(); 130 if (dao != null) { 131 return dao; 132 } 133 return this.daoFactory.apply(this); 134 } 135 136 @Override 137 protected boolean initializeInternal() throws PermissionsLoadingException { 138 try { 139 sql = getManager().dataSourceForUrl(config().connectionUrl); 140 141 // Provide database-implementation specific DAO 142 try (Connection conn = sql.getConnection()) { 143 final String database = conn.getMetaData().getDatabaseProductName().toLowerCase(); 144 this.daoFactory = daoImplementations.get(database); 145 if (this.daoFactory == null) { 146 throw new PermissionsLoadingException(Messages.DB_IMPL_NOT_SUPPORTED.tr(database)); 147 } 148 } 149 } catch (SQLException e) { 150 throw new PermissionsLoadingException(Messages.DB_CONNECTION_ERROR.tr(), e); 151 } 152 153 /*try (SqlDao conn = getDao()) { 154 conn.prepareStatement("ALTER TABLE `{permissions}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute(); 155 conn.prepareStatement("ALTER TABLE `{permissions_entity}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute(); 156 conn.prepareStatement("ALTER TABLE `{permissions_inheritance}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute(); 157 } catch (SQLException e) { 158 // Ignore, this MySQL version just doesn't support it. 159 }*/ 160 161 162 if (autoInitialize) { 163 try { 164 return initializeTables(); 165 } catch (SQLException e) { 166 throw new PermissionsLoadingException(Messages.ERROR_INITIALIZE_TABLES.tr(), e); 167 } 168 } else { 169 return true; 170 } 171 } 172 173 public boolean initializeTables() throws SQLException { 174 List<SchemaMigration> migrations = SchemaMigrations.getMigrations(); 175 // Initialize data, perform migrations 176 try (SqlDao dao = getDao()) { 177 int initialVersion = dao.getSchemaVersion(); 178 if (initialVersion == SqlConstants.VERSION_NOT_INITIALIZED) { 179 dao.initializeTables(); 180 dao.setSchemaVersion(VERSION_LATEST); 181 return false; 182 } else { 183 int finalVersion = dao.executeInTransaction(() -> { 184 int highestVersion = initialVersion; 185 for (int i = initialVersion + 1; i < migrations.size(); ++i) { 186 migrations.get(i).migrate(dao); 187 highestVersion = i; 188 } 189 return highestVersion; 190 }); 191 if (initialVersion != finalVersion) { 192 dao.setSchemaVersion(finalVersion); 193 getManager().logger().info(Messages.SCHEMA_UPDATE_SUCCESS.tr(initialVersion, finalVersion)); 194 } 195 return true; 196 } 197 } 198 } 199 200 public void setConnectionUrl(String connectionUrl) { 201 config().connectionUrl = connectionUrl; 202 } 203 204 DataSource getDataSource() { 205 return this.sql; 206 } 207 208 public String getTableName(String raw) { 209 return getTableName(raw, false); 210 } 211 212 public String getTableName(String raw, boolean legacyOnly) { 213 if (config().legacyAliases != null && config().legacyAliases.containsKey(raw)) { 214 return config().legacyAliases.get(raw); 215 } else if (legacyOnly) { 216 return raw; 217 } else { 218 return config().prefix() + raw; 219 } 220 } 221 222 String insertPrefix(String query) { 223 return queryPrefixCache.computeIfAbsent(query, qu -> BRACES_PATTERN.matcher(qu).replaceAll(config().prefix())); 224 } 225 226 @Override 227 protected CompletableFuture<ImmutableSubjectData> getDataInternal(String type, String identifier) { 228 return runAsync(() -> { 229 try (SqlDao dao = getDao()) { 230 final Optional<SqlSubjectRef<?>> ref = dao.getSubjectRef(type, identifier); 231 if (ref.isPresent()) { 232 return getDataForRef(dao, ref.get()); 233 } else { 234 return new SqlSubjectData(SqlSubjectRef.unresolved(this.manager(), type, identifier)); 235 } 236 } catch (SQLException e) { 237 throw new PermissionsLoadingException(Messages.ERROR_LOADING.tr(type, identifier)); 238 } 239 }); 240 } 241 242 private SqlSubjectData getDataForRef(SqlDao dao, SqlSubjectRef<?> ref) throws SQLException { 243 List<SqlSegment> segments = dao.getSegments(ref); 244 PMap<PSet<ContextValue<?>>, SqlSegment> contexts = PCollections.map(); 245 for (SqlSegment segment : segments) { 246 contexts = contexts.plus(segment.contexts(), segment); 247 } 248 249 return new SqlSubjectData(ref, contexts, PCollections.vector()); 250 251 } 252 253 @Override 254 protected CompletableFuture<ImmutableSubjectData> setDataInternal(String type, String identifier, ImmutableSubjectData data) { 255 // Cases: update data for sql (easy), update of another type (get SQL data, do update) 256 SqlSubjectData sqlData; 257 if (data instanceof SqlSubjectData) { 258 sqlData = (SqlSubjectData) data; 259 } else { 260 return runAsync(() -> { 261 try (SqlDao dao = getDao()) { 262 SqlSubjectRef<?> ref = dao.getOrCreateSubjectRef(type, identifier); 263 SqlSubjectData newData = getDataForRef(dao, ref); 264 newData = (SqlSubjectData) newData.mergeFrom(data); 265 newData.doUpdates(dao); 266 return newData; 267 } 268 }); 269 } 270 return runAsync(() -> { 271 try (SqlDao dao = getDao()) { 272 sqlData.doUpdates(dao); 273 return sqlData; 274 } 275 }); 276 } 277 278 @Override 279 public CompletableFuture<Boolean> isRegistered(String type, String identifier) { 280 return runAsync(() -> { 281 try (SqlDao dao = getDao()) { 282 return dao.getSubjectRef(type, identifier).isPresent(); 283 } 284 }); 285 } 286 287 @Override 288 public Stream<String> getAllIdentifiers(String type) { 289 try (SqlDao dao = getDao()) { 290 return dao.getAllIdentifiers(type).stream(); // TODO 291 } catch (SQLException e) { 292 return Stream.of(); 293 } 294 } 295 296 @Override 297 public Set<String> getRegisteredTypes() { 298 try (SqlDao dao = getDao()) { 299 return dao.getRegisteredTypes(); 300 } catch (SQLException e) { 301 return Collections.emptySet(); 302 } 303 304 } 305 306 @Override 307 public CompletableFuture<Set<String>> getDefinedContextKeys() { 308 return runAsync(() -> { 309 try (SqlDao dao = getDao()) { 310 return dao.getUsedContextKeys(); 311 } 312 }); 313 } 314 315 @Override 316 public Stream<Map.Entry<SubjectRef<?>, ImmutableSubjectData>> getAll() { 317 try (SqlDao dao = getDao()) { 318 Set<Map.Entry<SubjectRef<?>, ImmutableSubjectData>> builder = new HashSet<>(); 319 for (SqlSubjectRef<?> ref : dao.getAllSubjectRefs()) { 320 builder.add(UnmodifiableCollections.immutableMapEntry(ref, getDataForRef(dao, ref))); 321 } 322 return builder.stream(); 323 } catch (SQLException e) { 324 return Stream.of(); 325 } 326 } 327 328 @Override 329 protected CompletableFuture<RankLadder> getRankLadderInternal(String ladder) { 330 return runAsync(() -> { 331 try (SqlDao dao = getDao()) { 332 return dao.getRankLadder(ladder); 333 } 334 }); 335 } 336 337 @Override 338 protected CompletableFuture<RankLadder> setRankLadderInternal(final String ladder, final @Nullable RankLadder newLadder) { 339 return runAsync(() -> { 340 try (SqlDao dao = getDao()) { 341 dao.setRankLadder(ladder, newLadder); 342 return dao.getRankLadder(ladder); 343 } 344 }); 345 } 346 347 @Override 348 public Stream<String> getAllRankLadders() { 349 try (SqlDao dao = getDao()) { 350 return dao.getAllRankLadderNames().stream(); 351 } catch (SQLException e) { 352 return Stream.of(); 353 } 354 } 355 356 @Override 357 public CompletableFuture<Boolean> hasRankLadder(String ladder) { 358 return runAsync(() -> { 359 try (SqlDao dao = getDao()) { 360 return dao.hasEntriesForRankLadder(ladder); 361 } 362 }); 363 } 364 365 @Override 366 public CompletableFuture<ContextInheritance> getContextInheritanceInternal() { 367 return runAsync(() -> { 368 try (SqlDao dao = getDao()) { 369 return dao.getContextInheritance(); 370 } 371 }); 372 } 373 374 @Override 375 public CompletableFuture<ContextInheritance> setContextInheritanceInternal(ContextInheritance inheritance) { 376 return runAsync(() -> { 377 try (SqlDao dao = getDao()) { 378 SqlContextInheritance sqlInheritance; 379 if (inheritance instanceof SqlContextInheritance) { 380 sqlInheritance = (SqlContextInheritance) inheritance; 381 } else { 382 sqlInheritance = new SqlContextInheritance( 383 PCollections.asMap(inheritance.allParents(), (k, v) -> k, (k, v) -> PCollections.asVector(v)), 384 PCollections.vector((dao_, inheritance_) -> { 385 for (Map.Entry<ContextValue<?>, List<ContextValue<?>>> ent : inheritance_.allParents().entrySet()) { 386 dao_.setContextInheritance(ent.getKey(), PCollections.asVector(ent.getValue())); 387 } 388 })); 389 } 390 sqlInheritance.doUpdate(dao); 391 } 392 return inheritance; 393 }); 394 } 395 396 @Override 397 protected <T> T performBulkOperationSync(final Function<DataStore, T> function) throws Exception { 398 SqlDao dao = null; 399 try { 400 dao = getDao(); 401 heldDao.set(dao); 402 dao.holdOpen++; 403 return function.apply(this); 404 } finally { 405 if (dao != null) { 406 if (--dao.holdOpen == 0) { 407 this.heldDao.remove(); 408 } 409 try { 410 dao.close(); 411 } catch (SQLException ignore) { 412 // Not much we can do 413 } 414 } 415 } 416 } 417 418 @Override 419 public void close() { 420 this.queryPrefixCache.clear(); 421 this.heldDao.remove(); 422 } 423 424 public void setPrefix(String prefix) { 425 config().prefix = prefix; 426 config().realPrefix = null; 427 } 428 429 public void setAutoInitialize(boolean autoInitialize) { 430 this.autoInitialize = autoInitialize; 431 } 432}