Skip to content

Commit

Permalink
KEYCLOAK-14855 Added realm-specific localization texts which affect t…
Browse files Browse the repository at this point in the history
…exts in every part of the UI (admin console / login page / personal info page / email templates). Also new API endpoints and a new UI screen to manage the realm-specific localization texts were introduced.

Co-authored-by: Daniel Fesenmeyer <[email protected]>
  • Loading branch information
2 people authored and pedroigor committed Oct 30, 2020
1 parent 785f2e7 commit e131de9
Show file tree
Hide file tree
Showing 41 changed files with 1,428 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.admin.client.resource;

import java.util.List;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

public interface RealmLocalizationResource {

@GET
@Produces(MediaType.APPLICATION_JSON)
List<String> getRealmSpecificLocales();

@Path("{locale}")
@GET
@Produces(MediaType.APPLICATION_JSON)
Map<String, String> getRealmLocalizationTexts(final @PathParam("locale") String locale);


@Path("{locale}/{key}")
@GET
@Produces(MediaType.TEXT_PLAIN)
String getRealmLocalizationText(final @PathParam("locale") String locale, final @PathParam("key") String key);


@Path("{locale}")
@DELETE
void deleteRealmLocalizationTexts(@PathParam("locale") String locale);

@Path("{locale}/{key}")
@DELETE
void deleteRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key);

@Path("{locale}/{key}")
@PUT
@Consumes(MediaType.TEXT_PLAIN)
void saveRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key, String text);
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 279,7 @@ Response testLDAPConnection(@FormParam("action") String action, @FormParam("conn
@Path("keys")
KeyResource keys();

@Path("localization")
RealmLocalizationResource localization();

}
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 1645,35 @@ public Map<String, String> getAttributes() {
return cached.getAttributes();
}

@Override
public void patchRealmLocalizationTexts(String locale, Map<String, String> localizationTexts) {
getDelegateForUpdate();
updated.patchRealmLocalizationTexts(locale, localizationTexts);
}

@Override
public boolean removeRealmLocalizationTexts(String locale) {
getDelegateForUpdate();
return updated.removeRealmLocalizationTexts(locale);
}

@Override
public Map<String, Map<String, String>> getRealmLocalizationTexts() {
if (isUpdated()) return updated.getRealmLocalizationTexts();
return cached.getRealmLocalizationTexts();
}

@Override
public Map<String, String> getRealmLocalizationTextsByLocale(String locale) {
if (isUpdated()) return updated.getRealmLocalizationTextsByLocale(locale);

Map<String, String> localizationTexts = Collections.emptyMap();
if (cached.getRealmLocalizationTexts() != null && cached.getRealmLocalizationTexts().containsKey(locale)) {
localizationTexts = cached.getRealmLocalizationTexts().get(locale);
}
return Collections.unmodifiableMap(localizationTexts);
}

@Override
public String toString() {
return String.format("%s@x", getId(), hashCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 171,6 @@ public GroupProvider getGroupDelegate() {
return groupDelegate;
}


@Override
public void registerRealmInvalidation(String id, String name) {
cache.realmUpdated(id, name, invalidations);
Expand Down Expand Up @@ -1247,4 1246,51 @@ public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel cl
getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess);
}

@Override
public void saveLocalizationText(RealmModel realm, String locale, String key, String text) {
getRealmDelegate().saveLocalizationText(realm, locale, key, text);
registerRealmInvalidation(realm.getId(), locale);
}

@Override
public void saveLocalizationTexts(RealmModel realm, String locale, Map<String, String> localizationTexts) {
getRealmDelegate().saveLocalizationTexts(realm, locale, localizationTexts);
registerRealmInvalidation(realm.getId(), locale);
}

@Override
public boolean updateLocalizationText(RealmModel realm, String locale, String key, String text) {
boolean wasFound = getRealmDelegate().updateLocalizationText(realm, locale, key, text);
if (wasFound) {
registerRealmInvalidation(realm.getId(), locale);
}
return wasFound;
}

@Override
public boolean deleteLocalizationTextsByLocale(RealmModel realm, String locale) {
boolean wasDeleted = getRealmDelegate().deleteLocalizationTextsByLocale(realm, locale);
if(wasDeleted) {
registerRealmInvalidation(realm.getId(), locale);
}
return wasDeleted;
}

@Override
public boolean deleteLocalizationText(RealmModel realm, String locale, String key) {
boolean wasFound = getRealmDelegate().deleteLocalizationText(realm, locale, key);
if (wasFound) {
registerRealmInvalidation(realm.getId(), locale);
}
return wasFound;
}

@Override
public String getLocalizationTextsById(RealmModel realm, String locale, String key) {
Map<String, String> localizationTexts = getRealm(realm.getId()).getRealmLocalizationTextsByLocale(locale);
if(localizationTexts != null) {
return localizationTexts.get(key);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 163,8 @@ public Set<IdentityProviderMapperModel> getIdentityProviderMapperSet() {

private Map<String, Integer> userActionTokenLifespans;

protected Map<String, Map<String,String>> realmLocalizationTexts;

public CachedRealm(Long revision, RealmModel model) {
super(revision, model.getId());
name = model.getName();
Expand Down Expand Up @@ -301,6 303,7 @@ public CachedRealm(Long revision, RealmModel model) {
} catch (UnsupportedOperationException ex) {
}

realmLocalizationTexts = model.getRealmLocalizationTexts();
}

protected void cacheClientScopes(RealmModel model) {
Expand Down Expand Up @@ -718,4 721,8 @@ public Map<String, String> getAttributes() {
public boolean isAllowUserManagedAccess() {
return allowUserManagedAccess;
}

public Map<String, Map<String, String>> getRealmLocalizationTexts() {
return realmLocalizationTexts;
}
}
82 changes: 82 additions & 0 deletions model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 39,16 @@
import org.keycloak.models.jpa.entities.ClientScopeEntity;
import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.RealmEntity;
import org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.Root;

import java.util.*;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -858,6 862,84 @@ public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel cl
.executeUpdate();
}

private RealmLocalizationTextsEntity getRealmLocalizationTextsEntity(String locale, String realmId) {
RealmLocalizationTextsEntity.RealmLocalizationTextEntityKey key = new RealmLocalizationTextsEntity.RealmLocalizationTextEntityKey();
key.setRealmId(realmId);
key.setLocale(locale);
return em.find(RealmLocalizationTextsEntity.class, key);
}

@Override
public boolean updateLocalizationText(RealmModel realm, String locale, String key, String text) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if (entity != null && entity.getTexts() != null && entity.getTexts().containsKey(key)) {
entity.getTexts().put(key, text);

em.persist(entity);
return true;
} else {
return false;
}
}

@Override
public void saveLocalizationText(RealmModel realm, String locale, String key, String text) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if(entity == null) {
entity = new RealmLocalizationTextsEntity();
entity.setRealmId(realm.getId());
entity.setLocale(locale);
entity.setTexts(new HashMap<>());
}
entity.getTexts().put(key, text);
em.persist(entity);
}

@Override
public void saveLocalizationTexts(RealmModel realm, String locale, Map<String, String> localizationTexts) {
RealmLocalizationTextsEntity entity = new RealmLocalizationTextsEntity();
entity.setTexts(localizationTexts);
entity.setLocale(locale);
entity.setRealmId(realm.getId());
em.merge(entity);
}

@Override
public boolean deleteLocalizationTextsByLocale(RealmModel realm, String locale) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaDelete<RealmLocalizationTextsEntity> criteriaDelete =
builder.createCriteriaDelete(RealmLocalizationTextsEntity.class);
Root<RealmLocalizationTextsEntity> root = criteriaDelete.from(RealmLocalizationTextsEntity.class);

criteriaDelete.where(builder.and(
builder.equal(root.get("realmId"), realm.getId()),
builder.equal(root.get("locale"), locale)));
int linesUpdated = em.createQuery(criteriaDelete).executeUpdate();
return linesUpdated == 1?true:false;
}

@Override
public String getLocalizationTextsById(RealmModel realm, String locale, String key) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if (entity != null && entity.getTexts() != null && entity.getTexts().containsKey(key)) {
return entity.getTexts().get(key);
}
return null;
}

@Override
public boolean deleteLocalizationText(RealmModel realm, String locale, String key) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if (entity != null && entity.getTexts() != null && entity.getTexts().containsKey(key)) {
entity.getTexts().remove(key);

em.persist(entity);
return true;
} else {
return false;
}
}

private ClientInitialAccessModel entityToModel(ClientInitialAccessEntity entity) {
ClientInitialAccessModel model = new ClientInitialAccessModel();
model.setId(entity.getId());
Expand Down
47 changes: 47 additions & 0 deletions model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 2225,53 @@ private ComponentEntity getComponentEntity(String id) {
return c;
}

@Override
public void patchRealmLocalizationTexts(String locale, Map<String, String> localizationTexts) {
Map<String, RealmLocalizationTextsEntity> currentLocalizationTexts = realm.getRealmLocalizationTexts();
if(currentLocalizationTexts.containsKey(locale)) {
RealmLocalizationTextsEntity localizationTextsEntity = currentLocalizationTexts.get(locale);
localizationTextsEntity.getTexts().putAll(localizationTexts);

em.persist(localizationTextsEntity);
}
else {
RealmLocalizationTextsEntity realmLocalizationTextsEntity = new RealmLocalizationTextsEntity();
realmLocalizationTextsEntity.setRealmId(realm.getId());
realmLocalizationTextsEntity.setLocale(locale);
realmLocalizationTextsEntity.setTexts(localizationTexts);

em.persist(realmLocalizationTextsEntity);
}
}

@Override
public boolean removeRealmLocalizationTexts(String locale) {
if (locale == null) return false;
if (realm.getRealmLocalizationTexts().containsKey(locale))
{
em.remove(realm.getRealmLocalizationTexts().get(locale));
return true;
}
return false;
}

@Override
public Map<String, Map<String, String>> getRealmLocalizationTexts() {
Map<String, Map<String, String>> localizationTexts = new HashMap<>();
realm.getRealmLocalizationTexts().forEach((locale, localizationTextsEntity) -> {
localizationTexts.put(localizationTextsEntity.getLocale(), localizationTextsEntity.getTexts());
});
return localizationTexts;
}

@Override
public Map<String, String> getRealmLocalizationTextsByLocale(String locale) {
if (realm.getRealmLocalizationTexts().containsKey(locale)) {
return realm.getRealmLocalizationTexts().get(locale).getTexts();
}
return Collections.emptyMap();
}

@Override
public String toString() {
return String.format("%s@x", getId(), hashCode());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.models.jpa.converter;

import java.io.IOException;
import java.util.Map;
import javax.persistence.AttributeConverter;
import org.jboss.logging.Logger;
import org.keycloak.util.JsonSerialization;

public class MapStringConverter implements AttributeConverter<Map<String, String>, String> {
private static final Logger logger = Logger.getLogger(MapStringConverter.class);

@Override
public String convertToDatabaseColumn(Map<String, String> attribute) {
try {
return JsonSerialization.writeValueAsString(attribute);
} catch (IOException e) {
logger.error("Error while converting Map to JSON String: ", e);
return null;
}
}

@Override
public Map<String, String> convertToEntityAttribute(String dbData) {
try {
return JsonSerialization.readValue(dbData, Map.class);
} catch (IOException e) {
logger.error("Error while converting JSON String to Map: ", e);
return null;
}
}
}
Loading

0 comments on commit e131de9

Please sign in to comment.