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