or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication-sessions.mdcomponent-framework.mdcore-models.mdcredential-management.mdindex.mdorganization-management.mdprovider-framework.mdsession-management.mduser-storage.mdvalidation-framework.mdvault-integration.md

component-framework.mddocs/

0

# Component Framework

1

2

The component framework provides a configuration-driven approach to managing provider instances in Keycloak. It enables dynamic configuration of providers with validation, lifecycle management, and hierarchical organization.

3

4

## Core Component Interfaces

5

6

### ComponentFactory

7

8

Factory interface for component-based providers with configuration support.

9

10

```java { .api }

11

public interface ComponentFactory<T extends Provider> extends ProviderFactory<T> {

12

/**

13

* Creates a provider instance with the given component configuration.

14

*

15

* @param session the Keycloak session

16

* @param model the component model with configuration

17

* @return provider instance

18

*/

19

T create(KeycloakSession session, ComponentModel model);

20

21

/**

22

* Validates the component configuration.

23

*

24

* @param session the Keycloak session

25

* @param realm the realm

26

* @param model the component model to validate

27

* @throws ComponentValidationException if validation fails

28

*/

29

void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)

30

throws ComponentValidationException;

31

32

/**

33

* Called when a component instance is created.

34

*

35

* @param session the Keycloak session

36

* @param realm the realm

37

* @param model the component model

38

*/

39

void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model);

40

41

/**

42

* Called when a component configuration is updated.

43

*

44

* @param session the Keycloak session

45

* @param realm the realm

46

* @param oldModel the previous component model

47

* @param newModel the updated component model

48

*/

49

void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel);

50

51

/**

52

* Gets configuration properties for this component type.

53

*

54

* @return list of configuration properties

55

*/

56

@Override

57

default List<ProviderConfigProperty> getConfigMetadata() {

58

return Collections.emptyList();

59

}

60

61

/**

62

* Gets configuration properties specific to the component context.

63

*

64

* @param session the Keycloak session

65

* @param realm the realm

66

* @return list of configuration properties

67

*/

68

default List<ProviderConfigProperty> getConfigProperties(KeycloakSession session, RealmModel realm) {

69

return getConfigMetadata();

70

}

71

}

72

```

73

74

### SubComponentFactory

75

76

Factory for components that can have sub-components.

77

78

```java { .api }

79

public interface SubComponentFactory<T extends Provider, S extends Provider> extends ComponentFactory<T> {

80

/**

81

* Gets the provider class for sub-components.

82

*

83

* @return sub-component provider class

84

*/

85

Class<S> getSubComponentClass();

86

87

/**

88

* Gets supported sub-component types.

89

*

90

* @param session the Keycloak session

91

* @param realm the realm

92

* @param model the parent component model

93

* @return list of supported sub-component types

94

*/

95

List<String> getSubComponentTypes(KeycloakSession session, RealmModel realm, ComponentModel model);

96

97

/**

98

* Gets configuration properties for a sub-component type.

99

*

100

* @param session the Keycloak session

101

* @param realm the realm

102

* @param parent the parent component model

103

* @param subType the sub-component type

104

* @return list of configuration properties

105

*/

106

List<ProviderConfigProperty> getSubComponentConfigProperties(KeycloakSession session, RealmModel realm,

107

ComponentModel parent, String subType);

108

}

109

```

110

111

## Component Model

112

113

### ComponentModel

114

115

Model representing a component configuration.

116

117

```java { .api }

118

public class ComponentModel {

119

private String id;

120

private String name;

121

private String providerId;

122

private String providerType;

123

private String parentId;

124

private String subType;

125

private MultivaluedHashMap<String, String> config;

126

127

// Constructors

128

public ComponentModel() {

129

this.config = new MultivaluedHashMap<>();

130

}

131

132

public ComponentModel(ComponentModel copy) {

133

this.id = copy.id;

134

this.name = copy.name;

135

this.providerId = copy.providerId;

136

this.providerType = copy.providerType;

137

this.parentId = copy.parentId;

138

this.subType = copy.subType;

139

this.config = new MultivaluedHashMap<>(copy.config);

140

}

141

142

public ComponentModel(String id, String name, String parentId, String providerId, String providerType) {

143

this();

144

this.id = id;

145

this.name = name;

146

this.parentId = parentId;

147

this.providerId = providerId;

148

this.providerType = providerType;

149

}

150

151

// Getters and setters

152

public String getId() { return id; }

153

public void setId(String id) { this.id = id; }

154

155

public String getName() { return name; }

156

public void setName(String name) { this.name = name; }

157

158

public String getProviderId() { return providerId; }

159

public void setProviderId(String providerId) { this.providerId = providerId; }

160

161

public String getProviderType() { return providerType; }

162

public void setProviderType(String providerType) { this.providerType = providerType; }

163

164

public String getParentId() { return parentId; }

165

public void setParentId(String parentId) { this.parentId = parentId; }

166

167

public String getSubType() { return subType; }

168

public void setSubType(String subType) { this.subType = subType; }

169

170

public MultivaluedHashMap<String, String> getConfig() { return config; }

171

public void setConfig(MultivaluedHashMap<String, String> config) { this.config = config; }

172

173

// Configuration helper methods

174

public String get(String key) {

175

List<String> values = config.get(key);

176

return values != null && !values.isEmpty() ? values.get(0) : null;

177

}

178

179

public String get(String key, String defaultValue) {

180

String value = get(key);

181

return value != null ? value : defaultValue;

182

}

183

184

public List<String> getList(String key) {

185

List<String> values = config.get(key);

186

return values != null ? values : Collections.emptyList();

187

}

188

189

public int get(String key, int defaultValue) {

190

String value = get(key);

191

if (value == null) return defaultValue;

192

try {

193

return Integer.parseInt(value);

194

} catch (NumberFormatException e) {

195

return defaultValue;

196

}

197

}

198

199

public long get(String key, long defaultValue) {

200

String value = get(key);

201

if (value == null) return defaultValue;

202

try {

203

return Long.parseLong(value);

204

} catch (NumberFormatException e) {

205

return defaultValue;

206

}

207

}

208

209

public boolean get(String key, boolean defaultValue) {

210

String value = get(key);

211

return value != null ? Boolean.parseBoolean(value) : defaultValue;

212

}

213

214

public void put(String key, String value) {

215

if (value == null) {

216

config.remove(key);

217

} else {

218

config.putSingle(key, value);

219

}

220

}

221

222

public void put(String key, List<String> values) {

223

if (values == null || values.isEmpty()) {

224

config.remove(key);

225

} else {

226

config.put(key, values);

227

}

228

}

229

230

public void put(String key, int value) {

231

put(key, String.valueOf(value));

232

}

233

234

public void put(String key, long value) {

235

put(key, String.valueOf(value));

236

}

237

238

public void put(String key, boolean value) {

239

put(key, String.valueOf(value));

240

}

241

242

public boolean contains(String key) {

243

return config.containsKey(key);

244

}

245

246

@Override

247

public boolean equals(Object obj) {

248

if (this == obj) return true;

249

if (obj == null || getClass() != obj.getClass()) return false;

250

ComponentModel that = (ComponentModel) obj;

251

return Objects.equals(id, that.id);

252

}

253

254

@Override

255

public int hashCode() {

256

return Objects.hash(id);

257

}

258

259

@Override

260

public String toString() {

261

return String.format("ComponentModel{id='%s', name='%s', providerId='%s', providerType='%s'}",

262

id, name, providerId, providerType);

263

}

264

}

265

```

266

267

### ConfiguredComponent

268

269

Interface for components that require configuration.

270

271

```java { .api }

272

public interface ConfiguredComponent {

273

/**

274

* Gets the component model configuration.

275

*

276

* @return component model

277

*/

278

ComponentModel getModel();

279

}

280

```

281

282

### PrioritizedComponentModel

283

284

Component model with priority ordering.

285

286

```java { .api }

287

public class PrioritizedComponentModel extends ComponentModel {

288

private int priority;

289

290

public PrioritizedComponentModel() {

291

super();

292

}

293

294

public PrioritizedComponentModel(ComponentModel copy) {

295

super(copy);

296

if (copy instanceof PrioritizedComponentModel) {

297

this.priority = ((PrioritizedComponentModel) copy).priority;

298

}

299

}

300

301

public PrioritizedComponentModel(ComponentModel copy, int priority) {

302

super(copy);

303

this.priority = priority;

304

}

305

306

public int getPriority() { return priority; }

307

public void setPriority(int priority) { this.priority = priority; }

308

309

@Override

310

public String toString() {

311

return String.format("PrioritizedComponentModel{id='%s', name='%s', providerId='%s', priority=%d}",

312

getId(), getName(), getProviderId(), priority);

313

}

314

}

315

```

316

317

### JsonConfigComponentModel

318

319

Component model that stores configuration as JSON.

320

321

```java { .api }

322

public class JsonConfigComponentModel extends ComponentModel {

323

private Map<String, Object> jsonConfig;

324

325

public JsonConfigComponentModel() {

326

super();

327

this.jsonConfig = new HashMap<>();

328

}

329

330

public JsonConfigComponentModel(ComponentModel copy) {

331

super(copy);

332

this.jsonConfig = new HashMap<>();

333

}

334

335

public Map<String, Object> getJsonConfig() { return jsonConfig; }

336

public void setJsonConfig(Map<String, Object> jsonConfig) { this.jsonConfig = jsonConfig; }

337

338

public <T> T getJsonConfig(String key, Class<T> type) {

339

Object value = jsonConfig.get(key);

340

return type.isInstance(value) ? type.cast(value) : null;

341

}

342

343

public void putJsonConfig(String key, Object value) {

344

if (value == null) {

345

jsonConfig.remove(key);

346

} else {

347

jsonConfig.put(key, value);

348

}

349

}

350

}

351

```

352

353

## Exception Handling

354

355

### ComponentValidationException

356

357

Exception thrown during component validation.

358

359

```java { .api }

360

public class ComponentValidationException extends Exception {

361

private final Object[] parameters;

362

363

public ComponentValidationException(String message) {

364

super(message);

365

this.parameters = null;

366

}

367

368

public ComponentValidationException(String message, Object... parameters) {

369

super(message);

370

this.parameters = parameters;

371

}

372

373

public ComponentValidationException(String message, Throwable cause) {

374

super(message, cause);

375

this.parameters = null;

376

}

377

378

public ComponentValidationException(String message, Throwable cause, Object... parameters) {

379

super(message, cause);

380

this.parameters = parameters;

381

}

382

383

public Object[] getParameters() { return parameters; }

384

385

public String getLocalizedMessage() {

386

if (parameters != null && parameters.length > 0) {

387

return String.format(getMessage(), parameters);

388

}

389

return getMessage();

390

}

391

}

392

```

393

394

## Usage Examples

395

396

### Creating a Component Factory

397

398

```java

399

public class EmailProviderFactory implements ComponentFactory<EmailProvider> {

400

public static final String PROVIDER_ID = "smtp-email";

401

402

@Override

403

public EmailProvider create(KeycloakSession session, ComponentModel model) {

404

return new SmtpEmailProvider(session, model);

405

}

406

407

@Override

408

public String getId() {

409

return PROVIDER_ID;

410

}

411

412

@Override

413

public List<ProviderConfigProperty> getConfigMetadata() {

414

return Arrays.asList(

415

new ProviderConfigProperty("host", "SMTP Host", "SMTP server hostname",

416

ProviderConfigProperty.STRING_TYPE, "localhost"),

417

new ProviderConfigProperty("port", "SMTP Port", "SMTP server port",

418

ProviderConfigProperty.STRING_TYPE, "587"),

419

new ProviderConfigProperty("username", "Username", "SMTP username",

420

ProviderConfigProperty.STRING_TYPE, null),

421

new ProviderConfigProperty("password", "Password", "SMTP password",

422

ProviderConfigProperty.PASSWORD, null),

423

new ProviderConfigProperty("tls", "Enable TLS", "Enable TLS encryption",

424

ProviderConfigProperty.BOOLEAN_TYPE, true),

425

new ProviderConfigProperty("from", "From Address", "Email from address",

426

ProviderConfigProperty.STRING_TYPE, "noreply@example.com")

427

);

428

}

429

430

@Override

431

public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)

432

throws ComponentValidationException {

433

434

String host = model.get("host");

435

if (host == null || host.trim().isEmpty()) {

436

throw new ComponentValidationException("SMTP host is required");

437

}

438

439

String port = model.get("port");

440

if (port != null && !port.isEmpty()) {

441

try {

442

int portNum = Integer.parseInt(port);

443

if (portNum < 1 || portNum > 65535) {

444

throw new ComponentValidationException("Port must be between 1 and 65535");

445

}

446

} catch (NumberFormatException e) {

447

throw new ComponentValidationException("Invalid port number: " + port);

448

}

449

}

450

451

String fromAddress = model.get("from");

452

if (fromAddress == null || !fromAddress.contains("@")) {

453

throw new ComponentValidationException("Valid from address is required");

454

}

455

456

// Test SMTP connection

457

try {

458

testSmtpConnection(model);

459

} catch (Exception e) {

460

throw new ComponentValidationException("Failed to connect to SMTP server: " + e.getMessage(), e);

461

}

462

}

463

464

@Override

465

public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {

466

// Log component creation

467

logger.info("Created email provider component: " + model.getName());

468

}

469

470

@Override

471

public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {

472

// Handle configuration changes

473

if (!Objects.equals(oldModel.get("host"), newModel.get("host"))) {

474

logger.info("Email provider host changed from {} to {}",

475

oldModel.get("host"), newModel.get("host"));

476

}

477

}

478

479

private void testSmtpConnection(ComponentModel model) throws Exception {

480

// Implementation to test SMTP connection

481

Properties props = new Properties();

482

props.put("mail.smtp.host", model.get("host"));

483

props.put("mail.smtp.port", model.get("port", "587"));

484

props.put("mail.smtp.starttls.enable", model.get("tls", true));

485

486

String username = model.get("username");

487

String password = model.get("password");

488

489

Session mailSession;

490

if (username != null && password != null) {

491

props.put("mail.smtp.auth", "true");

492

mailSession = Session.getInstance(props, new Authenticator() {

493

@Override

494

protected PasswordAuthentication getPasswordAuthentication() {

495

return new PasswordAuthentication(username, password);

496

}

497

});

498

} else {

499

mailSession = Session.getInstance(props);

500

}

501

502

Transport transport = mailSession.getTransport("smtp");

503

transport.connect();

504

transport.close();

505

}

506

}

507

```

508

509

### Creating a Sub-Component Factory

510

511

```java

512

public class LdapProviderFactory implements SubComponentFactory<UserStorageProvider, UserStorageProviderModel> {

513

public static final String PROVIDER_ID = "ldap";

514

515

@Override

516

public UserStorageProvider create(KeycloakSession session, ComponentModel model) {

517

return new LdapUserStorageProvider(session, model);

518

}

519

520

@Override

521

public String getId() {

522

return PROVIDER_ID;

523

}

524

525

@Override

526

public Class<UserStorageProviderModel> getSubComponentClass() {

527

return UserStorageProviderModel.class;

528

}

529

530

@Override

531

public List<String> getSubComponentTypes(KeycloakSession session, RealmModel realm, ComponentModel model) {

532

return Arrays.asList("attribute-mapper", "group-mapper", "role-mapper");

533

}

534

535

@Override

536

public List<ProviderConfigProperty> getSubComponentConfigProperties(KeycloakSession session, RealmModel realm,

537

ComponentModel parent, String subType) {

538

switch (subType) {

539

case "attribute-mapper":

540

return getAttributeMapperProperties();

541

case "group-mapper":

542

return getGroupMapperProperties();

543

case "role-mapper":

544

return getRoleMapperProperties();

545

default:

546

return Collections.emptyList();

547

}

548

}

549

550

@Override

551

public List<ProviderConfigProperty> getConfigMetadata() {

552

return Arrays.asList(

553

new ProviderConfigProperty("connectionUrl", "Connection URL", "LDAP connection URL",

554

ProviderConfigProperty.STRING_TYPE, "ldap://localhost:389"),

555

new ProviderConfigProperty("bindDn", "Bind DN", "DN of LDAP admin user",

556

ProviderConfigProperty.STRING_TYPE, null),

557

new ProviderConfigProperty("bindCredential", "Bind Credential", "Password of LDAP admin user",

558

ProviderConfigProperty.PASSWORD, null),

559

new ProviderConfigProperty("usersDn", "Users DN", "DN where users are stored",

560

ProviderConfigProperty.STRING_TYPE, "ou=users,dc=example,dc=com"),

561

new ProviderConfigProperty("usernameAttribute", "Username Attribute", "LDAP attribute for username",

562

ProviderConfigProperty.STRING_TYPE, "uid")

563

);

564

}

565

566

private List<ProviderConfigProperty> getAttributeMapperProperties() {

567

return Arrays.asList(

568

new ProviderConfigProperty("ldap.attribute", "LDAP Attribute", "LDAP attribute name",

569

ProviderConfigProperty.STRING_TYPE, null),

570

new ProviderConfigProperty("user.model.attribute", "User Model Attribute", "User model attribute name",

571

ProviderConfigProperty.STRING_TYPE, null),

572

new ProviderConfigProperty("read.only", "Read Only", "Whether attribute is read-only",

573

ProviderConfigProperty.BOOLEAN_TYPE, false)

574

);

575

}

576

577

private List<ProviderConfigProperty> getGroupMapperProperties() {

578

return Arrays.asList(

579

new ProviderConfigProperty("groups.dn", "Groups DN", "DN where groups are stored",

580

ProviderConfigProperty.STRING_TYPE, "ou=groups,dc=example,dc=com"),

581

new ProviderConfigProperty("group.name.attribute", "Group Name Attribute", "LDAP attribute for group name",

582

ProviderConfigProperty.STRING_TYPE, "cn"),

583

new ProviderConfigProperty("membership.attribute", "Membership Attribute", "LDAP attribute for membership",

584

ProviderConfigProperty.STRING_TYPE, "member")

585

);

586

}

587

588

private List<ProviderConfigProperty> getRoleMapperProperties() {

589

return Arrays.asList(

590

new ProviderConfigProperty("roles.dn", "Roles DN", "DN where roles are stored",

591

ProviderConfigProperty.STRING_TYPE, "ou=roles,dc=example,dc=com"),

592

new ProviderConfigProperty("role.name.attribute", "Role Name Attribute", "LDAP attribute for role name",

593

ProviderConfigProperty.STRING_TYPE, "cn"),

594

new ProviderConfigProperty("use.realm.roles.mapping", "Use Realm Roles", "Map to realm roles",

595

ProviderConfigProperty.BOOLEAN_TYPE, true)

596

);

597

}

598

}

599

```

600

601

### Working with Components

602

603

```java

604

// Create and configure a component

605

try (KeycloakSession session = sessionFactory.create()) {

606

RealmModel realm = session.realms().getRealmByName("myrealm");

607

608

// Create email provider component

609

ComponentModel emailComponent = new ComponentModel();

610

emailComponent.setName("SMTP Email Provider");

611

emailComponent.setProviderId("smtp-email");

612

emailComponent.setProviderType("email");

613

emailComponent.setParentId(realm.getId());

614

615

// Configure SMTP settings

616

emailComponent.put("host", "smtp.gmail.com");

617

emailComponent.put("port", "587");

618

emailComponent.put("username", "myapp@gmail.com");

619

emailComponent.put("password", "app-password");

620

emailComponent.put("tls", true);

621

emailComponent.put("from", "myapp@gmail.com");

622

623

// Add the component to the realm

624

realm.addComponent(emailComponent);

625

626

// Get the email provider instance

627

EmailProvider emailProvider = session.getProvider(EmailProvider.class, emailComponent.getId());

628

629

// Use the provider

630

emailProvider.send("test@example.com", "Test Subject", "Test message");

631

}

632

```

633

634

### Component Hierarchy Management

635

636

```java

637

// Working with component hierarchies

638

try (KeycloakSession session = sessionFactory.create()) {

639

RealmModel realm = session.realms().getRealmByName("myrealm");

640

641

// Create parent component (LDAP provider)

642

ComponentModel ldapComponent = new ComponentModel();

643

ldapComponent.setName("LDAP User Storage");

644

ldapComponent.setProviderId("ldap");

645

ldapComponent.setProviderType("org.keycloak.storage.UserStorageProvider");

646

ldapComponent.setParentId(realm.getId());

647

648

// Configure LDAP settings

649

ldapComponent.put("connectionUrl", "ldap://ldap.example.com:389");

650

ldapComponent.put("bindDn", "cn=admin,dc=example,dc=com");

651

ldapComponent.put("bindCredential", "admin123");

652

ldapComponent.put("usersDn", "ou=users,dc=example,dc=com");

653

654

realm.addComponent(ldapComponent);

655

656

// Create sub-component (attribute mapper)

657

ComponentModel attributeMapper = new ComponentModel();

658

attributeMapper.setName("Email Attribute Mapper");

659

attributeMapper.setProviderId("user-attribute-ldap-mapper");

660

attributeMapper.setProviderType("org.keycloak.storage.ldap.mappers.LDAPStorageMapper");

661

attributeMapper.setParentId(ldapComponent.getId());

662

attributeMapper.setSubType("attribute-mapper");

663

664

// Configure attribute mapping

665

attributeMapper.put("ldap.attribute", "mail");

666

attributeMapper.put("user.model.attribute", "email");

667

attributeMapper.put("read.only", false);

668

669

realm.addComponent(attributeMapper);

670

671

// Get all sub-components of the LDAP provider

672

Stream<ComponentModel> subComponents = realm.getComponentsStream(ldapComponent.getId());

673

subComponents.forEach(comp -> {

674

System.out.println("Sub-component: " + comp.getName() + " (" + comp.getSubType() + ")");

675

});

676

}

677

```

678

679

### Component Configuration Validation

680

681

```java

682

// Custom component with validation

683

public class CustomComponentFactory implements ComponentFactory<CustomProvider> {

684

@Override

685

public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)

686

throws ComponentValidationException {

687

688

// Validate required fields

689

validateRequired(model, "apiKey", "API Key is required");

690

validateRequired(model, "endpoint", "Endpoint URL is required");

691

692

// Validate URL format

693

String endpoint = model.get("endpoint");

694

try {

695

new URL(endpoint);

696

} catch (MalformedURLException e) {

697

throw new ComponentValidationException("Invalid endpoint URL: " + endpoint);

698

}

699

700

// Validate numeric fields

701

validateNumeric(model, "timeout", 1, 300, "Timeout must be between 1 and 300 seconds");

702

validateNumeric(model, "maxRetries", 0, 10, "Max retries must be between 0 and 10");

703

704

// Validate enum values

705

String protocol = model.get("protocol");

706

if (protocol != null && !Arrays.asList("http", "https").contains(protocol.toLowerCase())) {

707

throw new ComponentValidationException("Protocol must be 'http' or 'https'");

708

}

709

710

// Custom business logic validation

711

validateBusinessRules(session, realm, model);

712

}

713

714

private void validateRequired(ComponentModel model, String key, String errorMessage)

715

throws ComponentValidationException {

716

String value = model.get(key);

717

if (value == null || value.trim().isEmpty()) {

718

throw new ComponentValidationException(errorMessage);

719

}

720

}

721

722

private void validateNumeric(ComponentModel model, String key, int min, int max, String errorMessage)

723

throws ComponentValidationException {

724

String value = model.get(key);

725

if (value != null && !value.isEmpty()) {

726

try {

727

int intValue = Integer.parseInt(value);

728

if (intValue < min || intValue > max) {

729

throw new ComponentValidationException(errorMessage);

730

}

731

} catch (NumberFormatException e) {

732

throw new ComponentValidationException("Invalid number format for " + key + ": " + value);

733

}

734

}

735

}

736

737

private void validateBusinessRules(KeycloakSession session, RealmModel realm, ComponentModel model)

738

throws ComponentValidationException {

739

// Custom validation logic specific to your component

740

String apiKey = model.get("apiKey");

741

if (apiKey != null && apiKey.length() < 32) {

742

throw new ComponentValidationException("API key must be at least 32 characters long");

743

}

744

745

// Check for conflicts with existing components

746

realm.getComponentsStream(realm.getId(), "custom-provider")

747

.filter(comp -> !comp.getId().equals(model.getId()))

748

.filter(comp -> comp.get("endpoint").equals(model.get("endpoint")))

749

.findFirst()

750

.ifPresent(existing -> {

751

throw new RuntimeException(new ComponentValidationException(

752

"Another component is already using this endpoint: " + existing.getName()));

753

});

754

}

755

}

756

```