/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.layout.dlm;
import com.google.common.cache.Cache;
import java.io.StringWriter;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import net.sf.ehcache.Ehcache;
import org.apache.commons.lang.StringUtils;
import org.apereo.portal.AuthorizationException;
import org.apereo.portal.IUserIdentityStore;
import org.apereo.portal.IUserProfile;
import org.apereo.portal.PortalException;
import org.apereo.portal.i18n.LocaleManager;
import org.apereo.portal.io.xml.IPortalDataHandlerService;
import org.apereo.portal.jdbc.RDBMServices;
import org.apereo.portal.layout.LayoutStructure;
import org.apereo.portal.layout.StructureParameter;
import org.apereo.portal.layout.StylesheetUserPreferencesImpl;
import org.apereo.portal.layout.dao.IStylesheetUserPreferencesDao;
import org.apereo.portal.layout.om.IStylesheetDescriptor;
import org.apereo.portal.layout.om.IStylesheetUserPreferences;
import org.apereo.portal.layout.simple.RDBMUserLayoutStore;
import org.apereo.portal.portlet.dao.IPortletEntityDao;
import org.apereo.portal.portlet.dao.jpa.PortletPreferenceImpl;
import org.apereo.portal.portlet.om.IPortletDefinition;
import org.apereo.portal.portlet.om.IPortletDefinitionId;
import org.apereo.portal.portlet.om.IPortletDefinitionParameter;
import org.apereo.portal.portlet.om.IPortletEntity;
import org.apereo.portal.portlet.om.IPortletPreference;
import org.apereo.portal.portlet.registry.IPortletEntityRegistry;
import org.apereo.portal.properties.PropertiesManager;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.provider.BrokenSecurityContext;
import org.apereo.portal.security.provider.PersonImpl;
import org.apereo.portal.utils.DocumentFactory;
import org.apereo.portal.utils.IFragmentDefinitionUtils;
import org.apereo.portal.utils.MapPopulator;
import org.apereo.portal.utils.Tuple;
import org.apereo.portal.xml.XmlUtilitiesImpl;
import org.dom4j.Namespace;
import org.dom4j.io.DOMReader;
import org.dom4j.io.DOMWriter;
import org.dom4j.io.DocumentSource;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* This class extends RDBMUserLayoutStore and implements instantiating and storing layouts that
* conform to the design of the Distributed Layout Management system. These layouts consist of two
* types: layout fragments that are the layouts owned by a user specified in in a
* fragment-definition.xml file, and composite view layouts which represent regular users with zero
* or more UI elements incorporated from layout fragments.
*
* @since 2.5
*/
public class RDBMDistributedLayoutStore extends RDBMUserLayoutStore {
private static final Pattern VALID_PATHREF_PATTERN = Pattern.compile(".+\\:/.+");
private static final String BAD_PATHREF_MESSAGE = "## DLM: ORPHANED DATA ##";
public static final String DEFAULT_LAYOUT_OWNER_PROPERTY = "org.apereo.portal.layout.dlm.defaultLayoutOwner";
private String systemDefaultUser = null;
private boolean systemDefaultUserLoaded = false;
private FragmentActivator fragmentActivator;
private Ehcache fragmentNodeInfoCache;
private boolean errorOnMissingPortlet = true;
private boolean errorOnMissingUser = true;
static final String TEMPLATE_USER_NAME =
"org.apereo.portal.services.Authentication.defaultTemplateUserName";
// Used in Import/Export operations
private final org.dom4j.DocumentFactory fac = new org.dom4j.DocumentFactory();
private final ThreadLocal<DOMReader> reader =
new ThreadLocal<DOMReader>() {
@Override
protected DOMReader initialValue() {
return new DOMReader();
}
};
private final ThreadLocal<DOMWriter> writer =
new ThreadLocal<DOMWriter>() {
@Override
protected DOMWriter initialValue() {
return new DOMWriter();
}
};
private IUserIdentityStore userIdentityStore;
private IStylesheetUserPreferencesDao stylesheetUserPreferencesDao;
private IPortletEntityRegistry portletEntityRegistry;
private IPortletEntityDao portletEntityDao;
private IPortalDataHandlerService portalDataHandlerService;
private IFragmentDefinitionUtils fragmentUtils;
@Autowired private NodeReferenceFactory nodeReferenceFactory;
@Autowired
public void setPortletEntityRegistry(IPortletEntityRegistry portletEntityRegistry) {
this.portletEntityRegistry = portletEntityRegistry;
}
@Autowired
public void setPortalDataHandlerService(IPortalDataHandlerService portalDataHandlerService) {
this.portalDataHandlerService = portalDataHandlerService;
}
@Autowired
public void setPortletEntityDao(@Qualifier("transient") IPortletEntityDao portletEntityDao) {
this.portletEntityDao = portletEntityDao;
}
@Autowired
public void setIdentityStore(IUserIdentityStore identityStore) {
this.userIdentityStore = identityStore;
}
@Autowired
public void setStylesheetUserPreferencesDao(
IStylesheetUserPreferencesDao stylesheetUserPreferencesDao) {
this.stylesheetUserPreferencesDao = stylesheetUserPreferencesDao;
}
@Autowired
public void setFragmentDefinitionUtils(IFragmentDefinitionUtils utils) {
this.fragmentUtils = utils;
}
@Autowired
public void setFragmentNodeInfoCache(
@Qualifier(
"org.apereo.portal.layout.dlm.RDBMDistributedLayoutStore.fragmentNodeInfoCache")
Ehcache fragmentNodeInfoCache) {
this.fragmentNodeInfoCache = fragmentNodeInfoCache;
}
@Value("${org.apereo.portal.io.layout.errorOnMissingPortlet:true}")
public void setErrorOnMissingPortlet(boolean errorOnMissingPortlet) {
this.errorOnMissingPortlet = errorOnMissingPortlet;
}
@Value("${org.apereo.portal.io.layout.errorOnMissingUser:true}")
public void setErrorOnMissingUser(boolean errorOnMissingUser) {
this.errorOnMissingUser = errorOnMissingUser;
}
/**
* Method for acquiring copies of fragment layouts to assist in debugging. No infrastructure
* code calls this but channels designed to expose the structure of the cached fragments use
* this to obtain copies.
*
* @return Map
*/
public Map<String, Document> getFragmentLayoutCopies() {
// since this is only visible in fragment list in administrative portlet, use default portal locale
final Locale defaultLocale = LocaleManager.getPortalLocales()[0];
final Map<String, Document> layouts = new HashMap<String, Document>();
final List<FragmentDefinition> definitions = this.fragmentUtils.getFragmentDefinitions();
for (final FragmentDefinition fragmentDefinition : definitions) {
final Document layout = DocumentFactory.getThreadDocument();
final UserView userView =
this.fragmentUtils.getUserView(fragmentDefinition, defaultLocale);
if (userView == null) {
logger.warn(
"No UserView found for FragmentDefinition {}, it will be skipped.",
fragmentDefinition.getName());
continue;
}
final Node copy = layout.importNode(userView.getLayout().getDocumentElement(), true);
layout.appendChild(copy);
layouts.put(fragmentDefinition.getOwnerId(), layout);
}
return layouts;
}
@Autowired
public void setFragmentActivator(FragmentActivator fragmentActivator) {
this.fragmentActivator = fragmentActivator;
}
private IStylesheetUserPreferences loadDistributedStylesheetUserPreferences(
IPerson person,
IUserProfile profile,
long stylesheetDescriptorId,
Set<String> fragmentNames) {
final boolean isFragmentOwner = this.isFragmentOwner(person);
final Locale locale = profile.getLocaleManager().getLocales()[0];
final IStylesheetDescriptor stylesheetDescriptor =
this.stylesheetDescriptorDao.getStylesheetDescriptor(stylesheetDescriptorId);
final IStylesheetUserPreferences stylesheetUserPreferences =
this.stylesheetUserPreferencesDao.getStylesheetUserPreferences(
stylesheetDescriptor, person, profile);
final IStylesheetUserPreferences distributedStylesheetUserPreferences =
new StylesheetUserPreferencesImpl();
for (final String fragName : fragmentNames) {
final FragmentDefinition fragmentDefinition =
this.fragmentUtils.getFragmentDefinitionByName(fragName);
//UserView may be missing if the fragment isn't defined correctly
final UserView userView = this.fragmentUtils.getUserView(fragmentDefinition, locale);
if (userView == null) {
logger.warn(
"No UserView is present for fragment {} it will be skipped when loading distributed stylesheet user preferences",
fragmentDefinition.getName());
continue;
}
//IStylesheetUserPreferences only exist if something was actually set
final IStylesheetUserPreferences fragmentStylesheetUserPreferences =
this.stylesheetUserPreferencesDao.getStylesheetUserPreferences(
stylesheetDescriptor, userView.getUserId(), userView.getProfileId());
if (fragmentStylesheetUserPreferences == null) {
continue;
}
//Get the info needed to DLMify node IDs
final Element root = userView.getLayout().getDocumentElement();
final String labelBase = root.getAttribute(Constants.ATT_ID);
boolean modified = false;
// Copy all of the fragment preferences into the distributed preferences
final Collection<String> allLayoutAttributeNodeIds =
fragmentStylesheetUserPreferences.getAllLayoutAttributeNodeIds();
for (final String fragmentNodeId : allLayoutAttributeNodeIds) {
final String userNodeId =
(isFragmentOwner
|| fragmentNodeId.startsWith(
Constants.FRAGMENT_ID_USER_PREFIX))
? fragmentNodeId
: labelBase + fragmentNodeId;
final MapPopulator<String, String> layoutAttributesPopulator =
new MapPopulator<String, String>();
fragmentStylesheetUserPreferences.populateLayoutAttributes(
fragmentNodeId, layoutAttributesPopulator);
final Map<String, String> layoutAttributes = layoutAttributesPopulator.getMap();
for (final Map.Entry<String, String> layoutAttributesEntry :
layoutAttributes.entrySet()) {
final String name = layoutAttributesEntry.getKey();
final String value = layoutAttributesEntry.getValue();
// Fragmentize the nodeId here
distributedStylesheetUserPreferences.setLayoutAttribute(
userNodeId, name, value);
// Clean out user preferences data that matches data from the fragment.
// Skip for fragment owners since their user preference data and the fragment stylesheet user prefs
// are identical and removing layout attributes here would affect the fragment layout.
if (stylesheetUserPreferences != null && !isFragmentOwner) {
final String userValue =
stylesheetUserPreferences.getLayoutAttribute(userNodeId, name);
if (userValue != null && userValue.equals(value)) {
stylesheetUserPreferences.removeLayoutAttribute(userNodeId, name);
EditManager.removePreferenceDirective(person, userNodeId, name);
modified = true;
}
}
}
}
if (modified) {
this.stylesheetUserPreferencesDao.storeStylesheetUserPreferences(
stylesheetUserPreferences);
}
}
return distributedStylesheetUserPreferences;
}
@Override
public double getFragmentPrecedence(int index) {
final List<FragmentDefinition> definitions = this.fragmentUtils.getFragmentDefinitions();
if (index < 0 || index > definitions.size() - 1) {
return 0;
}
// must pass through the array looking for the fragment with this
// index since the array was sorted by precedence and then index
// within precedence.
for (final FragmentDefinition fragmentDefinition : definitions) {
if (fragmentDefinition.getIndex() == index) {
return fragmentDefinition.getPrecedence();
}
}
return 0; // should never get here.
}
/**
* Returns the layout for a user decorated with any specified decorator. The layout returned is
* a composite layout for non fragment owners and a regular layout for layout owners. A
* composite layout is made up of layout pieces from potentially multiple incorporated layouts.
* If no layouts are defined then the composite layout will be the same as the user's personal
* layout fragment or PLF, the one holding only those UI elements that they own or incorporated
* elements that they have been allowed to changed.
*/
@Override
public DistributedUserLayout getUserLayout(IPerson person, IUserProfile profile) {
final DistributedUserLayout layout = this._getUserLayout(person, profile);
return layout;
}
private boolean layoutExistsForUser(IPerson person) {
// Assertions.
if (person == null) {
final String msg = "Argument 'person' cannot be null.";
throw new IllegalArgumentException(msg);
}
final int struct_count =
this.jdbcOperations.queryForObject(
"SELECT COUNT(*) FROM up_layout_struct WHERE user_id = ?",
Integer.class,
person.getID());
return struct_count == 0 ? false : true;
}
@Override
public org.dom4j.Element exportLayout(IPerson person, IUserProfile profile) {
org.dom4j.Element layout = getExportLayoutDom(person, profile);
final int userId = person.getID();
final String userName = person.getUserName();
final Set<IPortletEntity> portletEntities =
this.portletEntityDao.getPortletEntitiesForUser(userId);
org.dom4j.Element preferencesElement = null;
for (final Iterator<IPortletEntity> entityItr = portletEntities.iterator();
entityItr.hasNext();
) {
final IPortletEntity portletEntity = entityItr.next();
final List<IPortletPreference> preferencesList = portletEntity.getPortletPreferences();
//Only bother with entities that have preferences
if (!preferencesList.isEmpty()) {
final String layoutNodeId = portletEntity.getLayoutNodeId();
final Pathref dlmPathref =
nodeReferenceFactory.getPathrefFromNoderef(userName, layoutNodeId, layout);
if (dlmPathref == null) {
logger.warn(
"{} in user {}'s layout has no corresponding layout or portlet information and will be ignored",
portletEntity,
userName);
continue;
}
for (final IPortletPreference portletPreference : preferencesList) {
if (preferencesElement == null) {
if (layout == null) {
final org.dom4j.Document layoutDoc =
new org.dom4j.DocumentFactory().createDocument();
layout = layoutDoc.addElement("layout");
layout.addNamespace("dlm", Constants.NS_URI);
}
preferencesElement = layout.addElement("preferences");
}
final org.dom4j.Element preferenceEntry =
preferencesElement.addElement("entry");
preferenceEntry.addAttribute("entity", dlmPathref.toString());
preferenceEntry.addAttribute("channel", dlmPathref.getPortletFname());
preferenceEntry.addAttribute("name", portletPreference.getName());
for (final String value : portletPreference.getValues()) {
final org.dom4j.Element valueElement = preferenceEntry.addElement("value");
if (value != null) {
valueElement.setText(value);
}
}
}
}
}
if (layout != null) {
layout.addAttribute("script", "classpath://org/jasig/portal/io/import-layout_v3-2.crn");
layout.addAttribute("username", userName);
}
return layout;
}
private org.dom4j.Element getExportLayoutDom(IPerson person, IUserProfile profile) {
if (!this.layoutExistsForUser(person)) {
return null;
}
org.dom4j.Document layoutDoc = null;
try {
final Document layoutDom = this._safeGetUserLayout(person, profile);
person.setAttribute(Constants.PLF, layoutDom);
layoutDoc = this.reader.get().read(layoutDom);
} catch (final Throwable t) {
final String msg =
"Unable to obtain layout & profile for user '"
+ person.getUserName()
+ "', profileId "
+ profile.getProfileId();
throw new RuntimeException(msg, t);
}
if (logger.isDebugEnabled()) {
// Write out this version of the layout to the log for dev purposes...
final StringWriter str = new StringWriter();
final XMLWriter xml = new XMLWriter(str, new OutputFormat(" ", true));
try {
xml.write(layoutDoc);
xml.close();
} catch (final Throwable t) {
throw new RuntimeException(
"Failed to write the layout for user '"
+ person.getUserName()
+ "' to the DEBUG log",
t);
}
logger.debug(
"Layout for user: {}\n{}", person.getUserName(), str.getBuffer().toString());
}
/*
* Attempt to detect a corrupted layout; return null in such cases
*/
if (isLayoutCorrupt(layoutDoc)) {
logger.warn(
"Layout for user: {} is corrupt; layout structures will not be exported.",
person.getUserName());
return null;
}
/*
* Clean up the DOM for export.
*/
// (1) Add structure & theme attributes...
final int structureStylesheetId = profile.getStructureStylesheetId();
this.addStylesheetUserPreferencesAttributes(
person, profile, layoutDoc, structureStylesheetId, "structure");
final int themeStylesheetId = profile.getThemeStylesheetId();
this.addStylesheetUserPreferencesAttributes(
person, profile, layoutDoc, themeStylesheetId, "theme");
// (2) Remove locale info...
final Iterator<org.dom4j.Attribute> locale =
(Iterator<org.dom4j.Attribute>) layoutDoc.selectNodes("//@locale").iterator();
while (locale.hasNext()) {
final org.dom4j.Attribute loc = locale.next();
loc.getParent().remove(loc);
}
// (3) Scrub unnecessary channel information...
for (final Iterator<org.dom4j.Element> orphanedChannels =
(Iterator<org.dom4j.Element>)
layoutDoc.selectNodes("//channel[@fname = '']").iterator();
orphanedChannels.hasNext();
) {
// These elements represent UP_LAYOUT_STRUCT rows where the
// CHAN_ID field was not recognized by ChannelRegistryStore;
// best thing to do is remove the elements...
final org.dom4j.Element ch = orphanedChannels.next();
ch.getParent().remove(ch);
}
final List<String> channelAttributeWhitelist =
Arrays.asList(
new String[] {
"fname",
"unremovable",
"hidden",
"immutable",
"ID",
"dlm:plfID",
"dlm:moveAllowed",
"dlm:deleteAllowed"
});
final Iterator<org.dom4j.Element> channels =
(Iterator<org.dom4j.Element>) layoutDoc.selectNodes("//channel").iterator();
while (channels.hasNext()) {
final org.dom4j.Element oldCh = channels.next();
final org.dom4j.Element parent = oldCh.getParent();
final org.dom4j.Element newCh = this.fac.createElement("channel");
for (final String aName : channelAttributeWhitelist) {
final org.dom4j.Attribute a =
(org.dom4j.Attribute) oldCh.selectSingleNode("@" + aName);
if (a != null) {
newCh.addAttribute(a.getQName(), a.getValue());
}
}
parent.elements().add(parent.elements().indexOf(oldCh), newCh);
parent.remove(oldCh);
}
// (4) Convert internal DLM noderefs to external form (pathrefs)...
for (final Iterator<org.dom4j.Attribute> origins =
(Iterator<org.dom4j.Attribute>)
layoutDoc.selectNodes("//@dlm:origin").iterator();
origins.hasNext();
) {
final org.dom4j.Attribute org = origins.next();
final Pathref dlmPathref =
this.nodeReferenceFactory.getPathrefFromNoderef(
(String) person.getAttribute(IPerson.USERNAME),
org.getValue(),
layoutDoc.getRootElement());
if (dlmPathref != null) {
// Change the value only if we have a valid pathref...
org.setValue(dlmPathref.toString());
} else {
if (logger.isWarnEnabled()) {
logger.warn(
"Layout element '{}' from user '{}' failed to match noderef '{}'",
org.getUniquePath(),
person.getAttribute(IPerson.USERNAME),
org.getValue());
}
}
}
for (final Iterator<org.dom4j.Attribute> it =
(Iterator<org.dom4j.Attribute>)
layoutDoc.selectNodes("//@dlm:target").iterator();
it.hasNext();
) {
final org.dom4j.Attribute target = it.next();
final Pathref dlmPathref =
this.nodeReferenceFactory.getPathrefFromNoderef(
(String) person.getAttribute(IPerson.USERNAME),
target.getValue(),
layoutDoc.getRootElement());
if (dlmPathref != null) {
// Change the value only if we have a valid pathref...
target.setValue(dlmPathref.toString());
} else {
if (logger.isWarnEnabled()) {
logger.warn(
"Layout element '{}' from user '{}' failed to match noderef '{}'",
target.getUniquePath(),
person.getAttribute(IPerson.USERNAME),
target.getValue());
}
}
}
for (final Iterator<org.dom4j.Attribute> names =
(Iterator<org.dom4j.Attribute>)
layoutDoc.selectNodes("//dlm:*/@name").iterator();
names.hasNext();
) {
final org.dom4j.Attribute n = names.next();
if (n.getValue() == null || n.getValue().trim().length() == 0) {
// Outer <dlm:positionSet> elements don't seem to use the name
// attribute, though their childern do. Just skip these so we
// don't send a false WARNING.
continue;
}
final Pathref dlmPathref =
this.nodeReferenceFactory.getPathrefFromNoderef(
(String) person.getAttribute(IPerson.USERNAME),
n.getValue(),
layoutDoc.getRootElement());
if (dlmPathref != null) {
// Change the value only if we have a valid pathref...
n.setValue(dlmPathref.toString());
// These *may* have fnames...
if (dlmPathref.getPortletFname() != null) {
n.getParent().addAttribute("fname", dlmPathref.getPortletFname());
}
} else {
if (logger.isWarnEnabled()) {
logger.warn(
"Layout element '{}' from user '{}' failed to match noderef '{}'",
n.getUniquePath(),
person.getAttribute(IPerson.USERNAME),
n.getValue());
}
}
}
// Remove synthetic Ids, but from non-fragment owners only...
if (!this.isFragmentOwner(person)) {
/*
* In the case of fragment owners, the original database Ids allow
* us keep (not break) the associations that subscribers have with
* nodes on the fragment layout.
*/
// (5) Remove dlm:plfID...
for (final Iterator<org.dom4j.Attribute> plfid =
(Iterator<org.dom4j.Attribute>)
layoutDoc.selectNodes("//@dlm:plfID").iterator();
plfid.hasNext();
) {
final org.dom4j.Attribute plf = plfid.next();
plf.getParent().remove(plf);
}
// (6) Remove database Ids...
for (final Iterator<org.dom4j.Attribute> ids =
(Iterator<org.dom4j.Attribute>)
layoutDoc.selectNodes("//@ID").iterator();
ids.hasNext();
) {
final org.dom4j.Attribute a = ids.next();
a.getParent().remove(a);
}
}
return layoutDoc.getRootElement();
}
/**
* Attempts to detect known forms of corruption to avoid erroring-out on the export (or
* subsequent import), and also to prevent migrating a bad layout. Users whose layouts are
* culled in this fashion will have their layouts reset through migration.
*/
private boolean isLayoutCorrupt(org.dom4j.Document layoutDoc) {
boolean rslt = false; // until we find otherwise...
for (FormOfLayoutCorruption form : KNOWN_FORMS_OF_LAYOUT_CORRUPTION) {
if (form.detect(layoutDoc)) {
logger.warn("Corrupt layout detected: {}", form.getMessage());
rslt = true;
break;
}
}
return rslt;
}
private void addStylesheetUserPreferencesAttributes(
IPerson person,
IUserProfile profile,
org.dom4j.Document layoutDoc,
int stylesheetId,
String attributeType) {
final IStylesheetDescriptor structureStylesheetDescriptor =
this.stylesheetDescriptorDao.getStylesheetDescriptor(stylesheetId);
final IStylesheetUserPreferences ssup =
this.stylesheetUserPreferencesDao.getStylesheetUserPreferences(
structureStylesheetDescriptor, person, profile);
if (ssup != null) {
final Collection<String> allLayoutAttributeNodeIds =
ssup.getAllLayoutAttributeNodeIds();
for (final String nodeId : allLayoutAttributeNodeIds) {
final MapPopulator<String, String> layoutAttributesPopulator =
new MapPopulator<String, String>();
ssup.populateLayoutAttributes(nodeId, layoutAttributesPopulator);
final Map<String, String> layoutAttributes = layoutAttributesPopulator.getMap();
final org.dom4j.Element element = layoutDoc.elementByID(nodeId);
if (element == null) {
logger.warn(
"No node with id '{}' found in layout for: {}. Stylesheet user preference layout attributes will be ignored: {}",
nodeId,
person.getUserName(),
layoutAttributes);
continue;
}
for (final Entry<String, String> attributeEntry : layoutAttributes.entrySet()) {
final String name = attributeEntry.getKey();
final String value = attributeEntry.getValue();
logger.debug(
"Adding structure folder attribute: name={}, value={}", name, value);
final org.dom4j.Element structAttrElement =
this.fac.createElement(attributeType + "-attribute");
final org.dom4j.Element nameAttribute = structAttrElement.addElement("name");
nameAttribute.setText(name);
final org.dom4j.Element valueAttribute = structAttrElement.addElement("value");
valueAttribute.setText(value);
element.elements().add(0, structAttrElement);
}
}
} else {
logger.debug("no StylesheetUserPreferences found for {}, {}", person, profile);
}
}
@Override
@SuppressWarnings("unchecked")
@Transactional
public void importLayout(org.dom4j.Element layout) {
if (layout.getNamespaceForPrefix("dlm") == null) {
layout.add(new Namespace("dlm", Constants.NS_URI));
}
//Remove comments from the DOM they break import
final List<org.dom4j.Node> comments = layout.selectNodes("//comment()");
for (final org.dom4j.Node comment : comments) {
comment.detach();
}
//Get a ref to the prefs element and then remove it from the layout
final org.dom4j.Node preferencesElement = layout.selectSingleNode("preferences");
if (preferencesElement != null) {
preferencesElement.getParent().remove(preferencesElement);
}
final String ownerUsername = layout.valueOf("@username");
//Get a ref to the profile element and then remove it from the layout
final org.dom4j.Node profileElement = layout.selectSingleNode("profile");
if (profileElement != null) {
profileElement.getParent().remove(profileElement);
final org.dom4j.Document profileDocument =
new org.dom4j.DocumentFactory().createDocument();
profileDocument.setRootElement((org.dom4j.Element) profileElement);
profileDocument.setName(ownerUsername + ".profile");
final DocumentSource profileSource = new DocumentSource(profileElement);
this.portalDataHandlerService.importData(profileSource);
}
final IPerson person = new PersonImpl();
person.setUserName(ownerUsername);
int ownerId;
try {
//Can't just pass true for create here, if the user actually exists the create flag also updates the user data
ownerId = this.userIdentityStore.getPortalUID(person);
} catch (final AuthorizationException t) {
if (this.errorOnMissingUser) {
throw new RuntimeException(
"Unrecognized user "
+ person.getUserName()
+ "; you must import users before their layouts or set org.apereo.portal.io.layout.errorOnMissingUser to false.",
t);
}
//Create the missing user
ownerId = this.userIdentityStore.getPortalUID(person, true);
}
if (ownerId == -1) {
throw new RuntimeException(
"Unrecognized user "
+ person.getUserName()
+ "; you must import users before their layouts or set org.apereo.portal.io.layout.errorOnMissingUser to false.");
}
person.setID(ownerId);
IUserProfile profile = null;
try {
person.setSecurityContext(new BrokenSecurityContext());
profile = this.getUserProfileByFname(person, "default");
} catch (final Throwable t) {
throw new RuntimeException(
"Failed to load profile for "
+ person.getUserName()
+ "; This user must have a profile for import to continue.",
t);
}
// (6) Add database Ids & (5) Add dlm:plfID ...
int nextId = 1;
for (final Iterator<org.dom4j.Element> it =
(Iterator<org.dom4j.Element>)
layout.selectNodes("folder | dlm:* | channel").iterator();
it.hasNext();
) {
nextId = this.addIdAttributesIfNecessary(it.next(), nextId);
}
// Now update UP_USER...
this.jdbcOperations.update(
"UPDATE up_user SET next_struct_id = ? WHERE user_id = ?", nextId, person.getID());
// (4) Convert external DLM pathrefs to internal form (noderefs)...
for (final Iterator<org.dom4j.Attribute> itr =
(Iterator<org.dom4j.Attribute>)
layout.selectNodes("//@dlm:origin").iterator();
itr.hasNext();
) {
final org.dom4j.Attribute a = itr.next();
final Noderef dlmNoderef =
nodeReferenceFactory.getNoderefFromPathref(
ownerUsername, a.getValue(), null, true, layout);
if (dlmNoderef != null) {
// Change the value only if we have a valid pathref...
a.setValue(dlmNoderef.toString());
// For dlm:origin only, also use the noderef as the ID attribute...
a.getParent().addAttribute("ID", dlmNoderef.toString());
} else {
// At least insure the value is between 1 and 35 characters
a.setValue(BAD_PATHREF_MESSAGE);
}
}
for (final Iterator<org.dom4j.Attribute> itr =
(Iterator<org.dom4j.Attribute>)
layout.selectNodes("//@dlm:target").iterator();
itr.hasNext();
) {
final org.dom4j.Attribute a = itr.next();
final Noderef dlmNoderef =
nodeReferenceFactory.getNoderefFromPathref(
ownerUsername, a.getValue(), null, true, layout);
// Put in the correct value, or at least insure the value is between 1 and 35 characters
a.setValue(dlmNoderef != null ? dlmNoderef.toString() : BAD_PATHREF_MESSAGE);
}
for (final Iterator<org.dom4j.Attribute> names =
(Iterator<org.dom4j.Attribute>)
layout.selectNodes("//dlm:*/@name").iterator();
names.hasNext();
) {
final org.dom4j.Attribute a = names.next();
final String value = a.getValue().trim();
if (!VALID_PATHREF_PATTERN.matcher(value).matches()) {
/* Don't send it to getDlmNoderef if we know in advance it's not
* going to work; saves annoying/misleading log messages and
* possibly some processing. NOTE this is _only_ a problem with
* the name attribute of some dlm:* elements, which seems to go
* unused intentionally in some circumstances
*/
continue;
}
final org.dom4j.Attribute fname = a.getParent().attribute("fname");
Noderef dlmNoderef = null;
if (fname != null) {
dlmNoderef =
nodeReferenceFactory.getNoderefFromPathref(
ownerUsername, value, fname.getValue(), false, layout);
// Remove the fname attribute now that we're done w/ it...
fname.getParent().remove(fname);
} else {
dlmNoderef =
nodeReferenceFactory.getNoderefFromPathref(
ownerUsername, value, null, true, layout);
}
// Put in the correct value, or at least insure the value is between 1 and 35 characters
a.setValue(dlmNoderef != null ? dlmNoderef.toString() : BAD_PATHREF_MESSAGE);
}
// (3) Restore chanID attributes on <channel> elements...
for (final Iterator<org.dom4j.Element> it =
(Iterator<org.dom4j.Element>) layout.selectNodes("//channel").iterator();
it.hasNext();
) {
final org.dom4j.Element c = it.next();
final String fname = c.valueOf("@fname");
final IPortletDefinition cd =
this.portletDefinitionRegistry.getPortletDefinitionByFname(fname);
if (cd == null) {
final String msg =
"No portlet with fname="
+ fname
+ " exists referenced by node "
+ c.valueOf("@ID")
+ " from layout for "
+ ownerUsername;
if (errorOnMissingPortlet) {
throw new IllegalArgumentException(msg);
} else {
logger.warn(msg);
//Remove the bad channel node
c.getParent().remove(c);
}
} else {
c.addAttribute("chanID", String.valueOf(cd.getPortletDefinitionId().getStringId()));
}
}
// (2) Restore locale info...
// (This step doesn't appear to be needed for imports)
// (1) Process structure & theme attributes...
Document layoutDom = null;
try {
final int structureStylesheetId = profile.getStructureStylesheetId();
this.loadStylesheetUserPreferencesAttributes(
person, profile, layout, structureStylesheetId, "structure");
final int themeStylesheetId = profile.getThemeStylesheetId();
this.loadStylesheetUserPreferencesAttributes(
person, profile, layout, themeStylesheetId, "theme");
// From this point forward we need the user's PLF set as DLM expects it...
for (final Iterator<org.dom4j.Text> it =
(Iterator<org.dom4j.Text>)
layout.selectNodes("descendant::text()").iterator();
it.hasNext();
) {
// How many years have we used Java & XML, and this still isn't easy?
final org.dom4j.Text txt = it.next();
if (txt.getText().trim().length() == 0) {
txt.getParent().remove(txt);
}
}
final org.dom4j.Element copy = layout.createCopy();
final org.dom4j.Document doc = this.fac.createDocument(copy);
doc.normalize();
layoutDom = this.writer.get().write(doc);
person.setAttribute(Constants.PLF, layoutDom);
} catch (final Throwable t) {
throw new RuntimeException(
"Unable to set UserPreferences for user: " + person.getUserName(), t);
}
// Finally store the layout...
try {
this.setUserLayout(person, profile, layoutDom, true, true);
} catch (final Throwable t) {
final String msg = "Unable to persist layout for user: " + ownerUsername;
throw new RuntimeException(msg, t);
}
if (preferencesElement != null) {
final int ownerUserId = this.userIdentityStore.getPortalUserId(ownerUsername);
//TODO this assumes a single layout, when multi-layout support exists portlet entities will need to be re-worked to allow for a layout id to be associated with the entity
//track which entities from the user's pre-existing set are touched (all non-touched entities will be removed)
final Set<IPortletEntity> oldPortletEntities =
new LinkedHashSet<IPortletEntity>(
this.portletEntityDao.getPortletEntitiesForUser(ownerUserId));
final List<org.dom4j.Element> entries = preferencesElement.selectNodes("entry");
for (final org.dom4j.Element entry : entries) {
final String dlmPathRef = entry.attributeValue("entity");
final String fname = entry.attributeValue("channel");
final String prefName = entry.attributeValue("name");
final Noderef dlmNoderef =
nodeReferenceFactory.getNoderefFromPathref(
person.getUserName(), dlmPathRef, fname, false, layout);
if (dlmNoderef != null && fname != null) {
final IPortletEntity portletEntity =
this.getPortletEntity(fname, dlmNoderef.toString(), ownerUserId);
oldPortletEntities.remove(portletEntity);
final List<IPortletPreference> portletPreferences =
portletEntity.getPortletPreferences();
final List<org.dom4j.Element> valueElements = entry.selectNodes("value");
final List<String> values = new ArrayList<String>(valueElements.size());
for (final org.dom4j.Element valueElement : valueElements) {
values.add(valueElement.getText());
}
portletPreferences.add(
new PortletPreferenceImpl(
prefName, false, values.toArray(new String[values.size()])));
this.portletEntityDao.updatePortletEntity(portletEntity);
}
}
//Delete all portlet preferences for entities that were not imported
for (final IPortletEntity portletEntity : oldPortletEntities) {
portletEntity.setPortletPreferences(null);
if (portletEntityRegistry.shouldBePersisted(portletEntity)) {
this.portletEntityDao.updatePortletEntity(portletEntity);
} else {
this.portletEntityDao.deletePortletEntity(portletEntity);
}
}
}
}
private IPortletEntity getPortletEntity(String fName, String layoutNodeId, int userId) {
//Try getting the entity
final IPortletEntity portletEntity =
this.portletEntityDao.getPortletEntity(layoutNodeId, userId);
if (portletEntity != null) {
return portletEntity;
}
//Load the portlet definition
final IPortletDefinition portletDefinition;
try {
portletDefinition = this.portletDefinitionRegistry.getPortletDefinitionByFname(fName);
} catch (Exception e) {
throw new DataRetrievalFailureException(
"Failed to retrieve ChannelDefinition for fName='" + fName + "'", e);
}
//The channel definition for the fName MUST exist for this class to function
if (portletDefinition == null) {
throw new EmptyResultDataAccessException(
"No ChannelDefinition exists for fName='" + fName + "'", 1);
}
//create the portlet entity
final IPortletDefinitionId portletDefinitionId = portletDefinition.getPortletDefinitionId();
return this.portletEntityDao.createPortletEntity(portletDefinitionId, layoutNodeId, userId);
}
private void loadStylesheetUserPreferencesAttributes(
IPerson person,
IUserProfile profile,
org.dom4j.Element layout,
final int structureStylesheetId,
final String nodeType) {
final IStylesheetDescriptor stylesheetDescriptor =
this.stylesheetDescriptorDao.getStylesheetDescriptor(structureStylesheetId);
final List<org.dom4j.Element> structureAttributes =
layout.selectNodes("//" + nodeType + "-attribute");
IStylesheetUserPreferences ssup =
this.stylesheetUserPreferencesDao.getStylesheetUserPreferences(
stylesheetDescriptor, person, profile);
if (structureAttributes.isEmpty()) {
if (ssup != null) {
this.stylesheetUserPreferencesDao.deleteStylesheetUserPreferences(ssup);
}
} else {
if (ssup == null) {
ssup =
this.stylesheetUserPreferencesDao.createStylesheetUserPreferences(
stylesheetDescriptor, person, profile);
}
final Map<String, Map<String, String>> oldLayoutAttributes =
new HashMap<String, Map<String, String>>();
for (final String nodeId : ssup.getAllLayoutAttributeNodeIds()) {
final MapPopulator<String, String> nodeAttributes =
new MapPopulator<String, String>();
ssup.populateLayoutAttributes(nodeId, nodeAttributes);
oldLayoutAttributes.put(nodeId, nodeAttributes.getMap());
}
for (final org.dom4j.Element structureAttribute : structureAttributes) {
final org.dom4j.Element layoutElement = structureAttribute.getParent();
final String nodeId = layoutElement.valueOf("@ID");
if (StringUtils.isEmpty(nodeId)) {
logger.warn(
"@ID is empty for layout element, the attribute will be ignored: {}",
structureAttribute.asXML());
}
final String name = structureAttribute.valueOf("name");
if (StringUtils.isEmpty(nodeId)) {
logger.warn(
"name is empty for layout element, the attribute will be ignored: {}",
structureAttribute.asXML());
continue;
}
final String value = structureAttribute.valueOf("value");
if (StringUtils.isEmpty(nodeId)) {
logger.warn(
"value is empty for layout element, the attribute will be ignored: {}",
structureAttribute.asXML());
continue;
}
//Remove from the old attrs set as we've updated it
final Map<String, String> oldAttrs = oldLayoutAttributes.get(nodeId);
if (oldAttrs != null) {
oldAttrs.remove(name);
}
ssup.setLayoutAttribute(nodeId, name, value);
// Remove the layout attribute element or DLM fails
layoutElement.remove(structureAttribute);
}
//Purge orphaned entries
for (final Entry<String, Map<String, String>> oldAttributeEntry :
oldLayoutAttributes.entrySet()) {
final String nodeId = oldAttributeEntry.getKey();
for (final String name : oldAttributeEntry.getValue().keySet()) {
ssup.removeLayoutAttribute(nodeId, name);
}
}
this.stylesheetUserPreferencesDao.storeStylesheetUserPreferences(ssup);
}
}
@SuppressWarnings("unchecked")
private final int addIdAttributesIfNecessary(org.dom4j.Element e, int nextId) {
int idAfterThisOne = nextId; // default...
final org.dom4j.Node idAttribute = e.selectSingleNode("@ID | @dlm:plfID");
if (idAttribute == null) {
if (logger.isDebugEnabled()) {
logger.debug(
"No ID or dlm:plfID attribute for the following node (one will be generated and added): element{}, name={}, fname={}",
e.getName(),
e.valueOf("@name"),
e.valueOf("@fname"));
}
// We need to add an ID to this node...
char prefix;
if (e.getName().equals("folder")) {
prefix = 's';
} else if (e.getName().equals("channel")) {
prefix = 'n';
} else if (e.getQName().getNamespacePrefix().equals("dlm")) {
prefix = 'd';
} else {
throw new RuntimeException("Unrecognized element type: " + e.getName());
}
final String origin = e.valueOf("@dlm:origin");
// 'origin' may be null if the dlm:origin attribute is an
// empty string (which also shouldn't happen); 'origin'
// will be zero-length if dlm:origin is not defined...
if (origin != null && origin.length() != 0) {
// Add as dlm:plfID, if necessary...
e.addAttribute("dlm:plfID", prefix + String.valueOf(nextId));
} else {
// Do the standard thing, if necessary...
e.addAttribute("ID", prefix + String.valueOf(nextId));
}
++idAfterThisOne;
} else {
final String id = idAttribute.getText();
try {
idAfterThisOne = Integer.parseInt(id.substring(1)) + 1;
} catch (final NumberFormatException nfe) {
logger.warn(
"Could not parse int value from id: {} The next layout id will be: {}",
id,
idAfterThisOne,
nfe);
}
}
// Now check children...
for (final Iterator<org.dom4j.Element> itr =
(Iterator<org.dom4j.Element>)
e.selectNodes("folder | channel | dlm:*").iterator();
itr.hasNext();
) {
final org.dom4j.Element child = itr.next();
idAfterThisOne = this.addIdAttributesIfNecessary(child, idAfterThisOne);
}
return idAfterThisOne;
}
private final ThreadLocal<Cache<Tuple<String, String>, Document>> layoutCacheHolder =
new ThreadLocal<Cache<Tuple<String, String>, Document>>();
public void setLayoutImportExportCache(Cache<Tuple<String, String>, Document> layoutCache) {
if (layoutCache == null) {
layoutCacheHolder.remove();
} else {
this.layoutCacheHolder.set(layoutCache);
}
}
public Cache<Tuple<String, String>, Document> getLayoutImportExportCache() {
return layoutCacheHolder.get();
}
/**
* Handles locking and identifying proper root and namespaces that used to take place in super
* class.
*
* @param person
* @param profile
* @return @
*/
private Document _safeGetUserLayout(IPerson person, IUserProfile profile) {
Document layoutDoc;
Tuple<String, String> key = null;
final Cache<Tuple<String, String>, Document> layoutCache = getLayoutImportExportCache();
if (layoutCache != null) {
key = new Tuple<String, String>(person.getUserName(), profile.getProfileFname());
layoutDoc = layoutCache.getIfPresent(key);
if (layoutDoc != null) {
return (Document) layoutDoc.cloneNode(true);
}
}
layoutDoc = super.getPersonalUserLayout(person, profile);
Element layout = layoutDoc.getDocumentElement();
layout.setAttribute(Constants.NS_DECL, Constants.NS_URI);
if (layoutCache != null && key != null) {
layoutCache.put(key, (Document) layoutDoc.cloneNode(true));
}
return layoutDoc;
}
/**
* Returns the layout for a user. This method overrides the same method in the superclass to
* return a composite layout for non fragment owners and a regular layout for layout owners. A
* composite layout is made up of layout pieces from potentially multiple incorporated layouts.
* If no layouts are defined then the composite layout will be the same as the user's personal
* layout fragment or PLF, the one holding only those UI elements that they own or incorporated
* elements that they have been allowed to changed.
*/
private DistributedUserLayout _getUserLayout(IPerson person, IUserProfile profile) {
final String userName = (String) person.getAttribute("username");
final FragmentDefinition ownedFragment =
this.fragmentUtils.getFragmentDefinitionByOwner(person);
final boolean isLayoutOwnerDefault = this.isLayoutOwnerDefault(person);
final Set<String> fragmentNames = new LinkedHashSet<String>();
final Document ILF;
final Document PLF = this.getPLF(person, profile);
// If this user is an owner then ownedFragment will be non null. For
// fragment owners and owners of any default layout from which a
// fragment owners layout is copied there should not be any imported
// distributed layouts. Instead, load their PLF, mark as an owned
// if a fragment owner, and return.
if (ownedFragment != null || isLayoutOwnerDefault) {
ILF = (Document) PLF.cloneNode(true);
final Element layoutNode = ILF.getDocumentElement();
final Element ownerDocument = layoutNode.getOwnerDocument().getDocumentElement();
final NodeList channelNodes = ownerDocument.getElementsByTagName("channel");
for (int i = 0; i < channelNodes.getLength(); i++) {
Element channelNode = (Element) channelNodes.item(i);
final Node chanIdNode = channelNode.getAttributeNode("chanID");
if (chanIdNode == null
|| MissingPortletDefinition.CHANNEL_ID.equals(chanIdNode.getNodeValue())) {
channelNode.getParentNode().removeChild(channelNode);
}
}
if (ownedFragment != null) {
fragmentNames.add(ownedFragment.getName());
layoutNode.setAttributeNS(
Constants.NS_URI, Constants.ATT_FRAGMENT_NAME, ownedFragment.getName());
logger.debug(
"User '{}' is owner of '{}' fragment.", userName, ownedFragment.getName());
} else if (isLayoutOwnerDefault) {
layoutNode.setAttributeNS(Constants.NS_URI, Constants.ATT_IS_TEMPLATE_USER, "true");
layoutNode.setAttributeNS(
Constants.NS_URI,
Constants.ATT_TEMPLATE_LOGIN_ID,
(String) person.getAttribute("username"));
}
} else {
final Locale locale = profile.getLocaleManager().getLocales()[0];
final List<FragmentDefinition> applicableFragmentDefinitions =
this.fragmentUtils.getFragmentDefinitionsApplicableToPerson(person);
final List<Document> applicableLayouts =
this.fragmentUtils.getFragmentDefinitionUserViewLayouts(
applicableFragmentDefinitions, locale);
final IntegrationResult integrationResult = new IntegrationResult();
ILF = this.createCompositeILF(person, PLF, applicableLayouts, integrationResult);
// push optimizations made during merge back into db.
if (integrationResult.isChangedPLF()) {
if (logger.isDebugEnabled()) {
logger.debug(
"Saving PLF for {} due to changes during merge.",
person.getAttribute(IPerson.USERNAME));
}
super.setUserLayout(person, profile, PLF, false);
}
fragmentNames.addAll(
this.fragmentUtils.getFragmentNames(applicableFragmentDefinitions));
}
return this.createDistributedUserLayout(person, profile, ILF, fragmentNames);
}
private Document getPLF(final IPerson person, final IUserProfile profile) {
Document PLF = (Document) person.getAttribute(Constants.PLF);
if (null == PLF) {
PLF = this._safeGetUserLayout(person, profile);
person.setAttribute(Constants.PLF, PLF);
}
if (logger.isDebugEnabled()) {
logger.debug(
"PLF for {} immediately after loading\n{}",
person.getAttribute(IPerson.USERNAME),
XmlUtilitiesImpl.toString(PLF));
}
return PLF;
}
/**
* Creates a composite ILF (incorporated layouts fragment) by first using the applicable
* fragment layouts, then merging in the PLF (personal layout fragment).
*/
private Document createCompositeILF(
final IPerson person,
final Document PLF,
final List<Document> applicableLayouts,
final IntegrationResult integrationResult) {
final Document ILF = ILFBuilder.constructILF(PLF, applicableLayouts, person);
PLFIntegrator.mergePLFintoILF(PLF, ILF, integrationResult);
if (logger.isDebugEnabled()) {
logger.debug(
"PLF for {} after MERGING\n{}",
person.getAttribute(IPerson.USERNAME),
XmlUtilitiesImpl.toString(PLF));
logger.debug(
"ILF for {} after MERGING\n{}",
person.getAttribute(IPerson.USERNAME),
XmlUtilitiesImpl.toString(ILF));
}
return ILF;
}
private DistributedUserLayout createDistributedUserLayout(
final IPerson person,
final IUserProfile profile,
final Document ILF,
final Set<String> fragmentNames) {
final int structureStylesheetId = profile.getStructureStylesheetId();
final IStylesheetUserPreferences distributedStructureStylesheetUserPreferences =
this.loadDistributedStylesheetUserPreferences(
person, profile, structureStylesheetId, fragmentNames);
final int themeStylesheetId = profile.getThemeStylesheetId();
final IStylesheetUserPreferences distributedThemeStylesheetUserPreferences =
this.loadDistributedStylesheetUserPreferences(
person, profile, themeStylesheetId, fragmentNames);
return new DistributedUserLayout(
ILF,
fragmentNames,
distributedStructureStylesheetUserPreferences,
distributedThemeStylesheetUserPreferences);
}
/**
* Convenience method for fragment activator to obtain raw layouts for fragments during
* initialization.
*/
public Document getFragmentLayout(IPerson person, IUserProfile profile) {
return this._safeGetUserLayout(person, profile);
}
/**
* Generates a new struct id for directive elements that dlm places in the PLF version of the
* layout tree. These elements are atifacts of the dlm storage model and used during merge but
* do not appear in the user's composite view.
*/
@Override
public String getNextStructDirectiveId(IPerson person) {
return super.getNextStructId(person, Constants.DIRECTIVE_PREFIX);
}
/**
* Replaces the layout Document stored on a fragment definition with a new version. This is
* called when a fragment owner updates their layout.
*/
private void updateCachedLayout(
Document layout, IUserProfile profile, FragmentDefinition fragment) {
final Locale locale = profile.getLocaleManager().getLocales()[0];
// need to make a copy that we can fragmentize
layout = (Document) layout.cloneNode(true);
// Fix later to handle multiple profiles
final Element root = layout.getDocumentElement();
final UserView userView = this.fragmentUtils.getUserView(fragment, locale);
if (userView == null) {
throw new IllegalStateException(
"No UserView found for fragment: " + fragment.getName());
}
root.setAttribute(
Constants.ATT_ID,
Constants.FRAGMENT_ID_USER_PREFIX
+ userView.getUserId()
+ Constants.FRAGMENT_ID_LAYOUT_PREFIX
+ "1");
try {
this.fragmentActivator.clearChacheForOwner(fragment.getOwnerId());
this.fragmentUtils.getUserView(fragment, locale);
} catch (final Exception e) {
logger.error("An exception occurred attempting to update a layout.", e);
}
}
/**
* Returns true is the user is the owner of a layout which is copied as the default for any
* fragment when first created.
*/
private boolean isLayoutOwnerDefault(IPerson person) {
final String userName = (String) person.getAttribute("username");
final List<FragmentDefinition> definitions = this.fragmentUtils.getFragmentDefinitions();
if (userName != null && definitions != null) {
for (final FragmentDefinition fragmentDefinition : definitions) {
if (fragmentDefinition.defaultLayoutOwnerID != null
&& fragmentDefinition.defaultLayoutOwnerID.equals(userName)) {
return true;
}
}
}
final String globalDefault =
PropertiesManager.getProperty(DEFAULT_LAYOUT_OWNER_PROPERTY);
if (globalDefault != null && globalDefault.equals(userName)) {
return true;
}
if (!this.systemDefaultUserLoaded) {
this.systemDefaultUserLoaded = true;
try {
this.systemDefaultUser = PropertiesManager.getProperty(TEMPLATE_USER_NAME);
} catch (final RuntimeException re) {
logger.error("Property '{}' not defined.", TEMPLATE_USER_NAME, re);
}
if (this.systemDefaultUser != null && this.systemDefaultUser.equals(userName)) {
return true;
}
}
return false;
}
@Override
public boolean isFragmentOwner(IPerson person) {
return this.fragmentUtils.getFragmentDefinitionByOwner(person) != null;
}
@Override
public boolean isFragmentOwner(String username) {
boolean rslt = false; // default
final List<FragmentDefinition> definitions = this.fragmentUtils.getFragmentDefinitions();
if (definitions != null) {
for (final FragmentDefinition fragmentDefinition : definitions) {
if (fragmentDefinition.getOwnerId().equals(username)) {
rslt = true;
break;
}
}
}
return rslt;
}
/**
* This method overrides the same method in the super class to persist only layout information
* stored in the user's person layout fragment or PLF. If this person is a layout owner then
* their changes are pushed into the appropriate layout fragment.
*/
public void setUserLayout(
IPerson person, IUserProfile profile, Document layoutXML, boolean channelsAdded) {
this.setUserLayout(person, profile, layoutXML, channelsAdded, true);
}
/**
* This method overrides the same method in the super class to persist only layout information
* stored in the user's person layout fragment or PLF. If fragment cache update is requested
* then it checks to see if this person is a layout owner and if so then their changes are
* pushed into the appropriate layout fragment.
*/
@Override
public void setUserLayout(
IPerson person,
IUserProfile profile,
Document layoutXML,
boolean channelsAdded,
boolean updateFragmentCache) {
final Document plf = (Document) person.getAttribute(Constants.PLF);
if (logger.isDebugEnabled()) {
logger.debug(
"PLF for {}\n{}",
person.getAttribute(IPerson.USERNAME),
XmlUtilitiesImpl.toString(plf));
}
super.setUserLayout(person, profile, plf, channelsAdded);
if (updateFragmentCache) {
final FragmentDefinition fragment =
this.fragmentUtils.getFragmentDefinitionByOwner(person);
if (fragment != null) {
this.updateCachedLayout(plf, profile, fragment);
}
}
}
@Override
public FragmentChannelInfo getFragmentChannelInfo(String sId) {
final FragmentNodeInfo node = this.getFragmentNodeInfo(sId);
if (node != null && node instanceof FragmentChannelInfo) {
return (FragmentChannelInfo) node;
}
return null;
}
@Override
public FragmentNodeInfo getFragmentNodeInfo(String sId) {
// grab local pointers to variables subject to change at any time
final List<FragmentDefinition> fragments = this.fragmentUtils.getFragmentDefinitions();
final Locale defaultLocale = LocaleManager.getPortalLocales()[0];
final net.sf.ehcache.Element element = this.fragmentNodeInfoCache.get(sId);
FragmentNodeInfo info =
element != null ? (FragmentNodeInfo) element.getObjectValue() : null;
if (info == null) {
for (final FragmentDefinition fragmentDefinition : fragments) {
final UserView userView =
this.fragmentUtils.getUserView(fragmentDefinition, defaultLocale);
if (userView == null) {
logger.warn(
"No UserView is present for fragment {} it will be skipped when fragment node information",
fragmentDefinition.getName());
continue;
}
final Element node = userView.getLayout().getElementById(sId);
if (node != null) // found it
{
if (node.getTagName().equals(Constants.ELM_CHANNEL)) {
info = new FragmentChannelInfo(node);
} else {
info = new FragmentNodeInfo(node);
}
this.fragmentNodeInfoCache.put(new net.sf.ehcache.Element(sId, info));
break;
}
}
}
return info;
}
@Override
protected Element getStructure(Document doc, LayoutStructure ls) {
Element structure = null;
String type = ls.getType();
if (ls.isChannel()) {
final IPortletDefinition channelDef =
this.portletDefinitionRegistry.getPortletDefinition(
String.valueOf(ls.getChanId()));
if (channelDef != null && channelApproved(channelDef.getApprovalDate())) {
structure =
this.getElementForChannel(
doc, channelPrefix + ls.getStructId(), channelDef, ls.getLocale());
} else {
structure =
this.getElementForChannel(
doc,
channelPrefix + ls.getStructId(),
MissingPortletDefinition.INSTANCE,
null);
}
} else {
// create folder objects including dlm new types in cp namespace
if (type != null && type.startsWith(Constants.NS)) {
structure = doc.createElementNS(Constants.NS_URI, type);
} else {
structure = doc.createElement("folder");
}
structure.setAttribute("name", ls.getName());
structure.setAttribute("type", (type != null ? type : "regular"));
}
structure.setAttribute("hidden", (ls.isHidden() ? "true" : "false"));
structure.setAttribute("immutable", (ls.isImmutable() ? "true" : "false"));
structure.setAttribute("unremovable", (ls.isUnremovable() ? "true" : "false"));
if (localeAware) {
structure.setAttribute("locale", ls.getLocale()); // for i18n by Shoji
}
/*
* Parameters from up_layout_param are loaded slightly differently for
* folders and channels. For folders all parameters are added as attributes
* of the Element. For channels only those parameters with names starting
* with the dlm namespace Constants.NS are added as attributes to the Element.
* Others are added as child parameter Elements.
*/
if (ls.getParameters() != null) {
for (final Iterator itr = ls.getParameters().iterator(); itr.hasNext(); ) {
final StructureParameter sp = (StructureParameter) itr.next();
String pName = sp.getName();
if (!ls.isChannel()) { // Folder
if (pName.startsWith(Constants.NS)) {
structure.setAttributeNS(Constants.NS_URI, pName, sp.getValue());
} else {
structure.setAttribute(pName, sp.getValue());
}
} else // Channel
{
// if dealing with a dlm namespace param add as attribute
if (pName.startsWith(Constants.NS)) {
structure.setAttributeNS(Constants.NS_URI, pName, sp.getValue());
itr.remove();
} else {
/*
* do traditional override processing. some explanation is in
* order. The structure element was created by the
* ChannelDefinition and only contains parameter children if the
* definition had defined parameters. These are checked for each
* layout loaded parameter as found in LayoutStructure.parameters.
* If a name match is found then we need to see if overriding is
* allowed and if so we set the value on the child parameter
* element. At that point we are done with that version loaded
* from the layout so we remove it from the in-memory set of
* parameters that are being merged-in. Then, after all such have
* been checked against those added by the channel definition we
* add in any remaining as adhoc, unregulated parameters.
*/
final NodeList nodeListParameters =
structure.getElementsByTagName("parameter");
for (int j = 0; j < nodeListParameters.getLength(); j++) {
final Element parmElement = (Element) nodeListParameters.item(j);
final NamedNodeMap nm = parmElement.getAttributes();
final String nodeName = nm.getNamedItem("name").getNodeValue();
if (nodeName.equals(pName)) {
final Node override = nm.getNamedItem("override");
if (override != null && override.getNodeValue().equals("yes")) {
final Node valueNode = nm.getNamedItem("value");
valueNode.setNodeValue(sp.getValue());
}
itr.remove();
break; // found the corresponding one so skip the rest
}
}
}
}
}
// For channels, add any remaining parameter elements loaded with the
// layout as adhoc, unregulated, parameter children that can be overridden.
if (ls.isChannel()) {
for (final Iterator itr = ls.getParameters().iterator(); itr.hasNext(); ) {
final StructureParameter sp = (StructureParameter) itr.next();
final Element parameter = doc.createElement("parameter");
parameter.setAttribute("name", sp.getName());
parameter.setAttribute("value", sp.getValue());
parameter.setAttribute("override", "yes");
structure.appendChild(parameter);
}
}
}
// finish setting up elements based on loaded params
final String origin = structure.getAttribute(Constants.ATT_ORIGIN);
final String prefix = ls.isChannel() ? channelPrefix : folderPrefix;
// if not null we are dealing with a node incorporated from another
// layout and this node contains changes made by the user so handle
// id swapping.
if (!origin.equals("")) {
structure.setAttributeNS(
Constants.NS_URI, Constants.ATT_PLF_ID, prefix + ls.getStructId());
structure.setAttribute("ID", origin);
} else if (!ls.isChannel())
// regular folder owned by this user, need to check if this is a
// directive or ui element. If the latter then use traditional id
// structure
{
if (type != null && type.startsWith(Constants.NS)) {
structure.setAttribute("ID", Constants.DIRECTIVE_PREFIX + ls.getStructId());
} else {
structure.setAttribute("ID", folderPrefix + ls.getStructId());
}
} else {
logger.debug("Adding identifier {}{}", folderPrefix, ls.getStructId());
structure.setAttribute("ID", channelPrefix + ls.getStructId());
}
structure.setIdAttribute(Constants.ATT_ID, true);
return structure;
}
@Override
protected int saveStructure(Node node, PreparedStatement structStmt, PreparedStatement parmStmt)
throws SQLException {
if (node == null) { // No more
return 0;
}
if (node.getNodeName().equals("parameter")) {
//parameter, skip it and go on to the next node
return this.saveStructure(node.getNextSibling(), structStmt, parmStmt);
}
if (!(node instanceof Element)) {
return 0;
}
final Element structure = (Element) node;
if (logger.isDebugEnabled()) {
logger.debug("saveStructure XML content: {}", XmlUtilitiesImpl.toString(node));
}
// determine the struct_id for storing in the db. For incorporated nodes in
// the plf their ID is a system-wide unique ID while their struct_id for
// storing in the db is cached in a dlm:plfID attribute.
int saveStructId = -1;
final String plfID = structure.getAttribute(Constants.ATT_PLF_ID);
if (!plfID.equals("")) {
saveStructId = Integer.parseInt(plfID.substring(1));
} else {
final String id = structure.getAttribute("ID");
saveStructId = Integer.parseInt(id.substring(1));
}
int nextStructId = 0;
int childStructId = 0;
int chanId = -1;
IPortletDefinition portletDef = null;
final boolean isChannel = node.getNodeName().equals("channel");
if (isChannel) {
chanId = Integer.parseInt(node.getAttributes().getNamedItem("chanID").getNodeValue());
portletDef =
this.portletDefinitionRegistry.getPortletDefinition(String.valueOf(chanId));
if (portletDef == null) {
//Portlet doesn't exist any more, drop the layout node
return 0;
}
}
if (node.hasChildNodes()) {
childStructId = this.saveStructure(node.getFirstChild(), structStmt, parmStmt);
}
nextStructId = this.saveStructure(node.getNextSibling(), structStmt, parmStmt);
structStmt.clearParameters();
structStmt.setInt(1, saveStructId);
structStmt.setInt(2, nextStructId);
structStmt.setInt(3, childStructId);
final String externalId = structure.getAttribute("external_id");
if (externalId != null && externalId.trim().length() > 0) {
final Integer eID = new Integer(externalId);
structStmt.setInt(4, eID.intValue());
} else {
structStmt.setNull(4, java.sql.Types.NUMERIC);
}
if (isChannel) {
structStmt.setInt(5, chanId);
structStmt.setNull(6, java.sql.Types.VARCHAR);
} else {
structStmt.setNull(5, java.sql.Types.NUMERIC);
structStmt.setString(6, structure.getAttribute("name"));
}
final String structType = structure.getAttribute("type");
structStmt.setString(7, structType);
structStmt.setString(8, RDBMServices.dbFlag(xmlBool(structure.getAttribute("hidden"))));
structStmt.setString(9, RDBMServices.dbFlag(xmlBool(structure.getAttribute("immutable"))));
structStmt.setString(
10, RDBMServices.dbFlag(xmlBool(structure.getAttribute("unremovable"))));
logger.debug(structStmt.toString());
structStmt.executeUpdate();
// code to persist extension attributes for dlm
final NamedNodeMap attribs = node.getAttributes();
for (int i = 0; i < attribs.getLength(); i++) {
final Node attrib = attribs.item(i);
final String name = attrib.getNodeName();
if (name.startsWith(Constants.NS)
&& !name.equals(Constants.ATT_PLF_ID)
&& !name.equals(Constants.ATT_FRAGMENT)
&& !name.equals(Constants.ATT_PRECEDENCE)) {
// a cp extension attribute. Push into param table.
parmStmt.clearParameters();
parmStmt.setInt(1, saveStructId);
parmStmt.setString(2, name);
parmStmt.setString(3, attrib.getNodeValue());
logger.debug(parmStmt.toString());
parmStmt.executeUpdate();
}
}
final NodeList parameters = node.getChildNodes();
if (parameters != null && isChannel) {
for (int i = 0; i < parameters.getLength(); i++) {
if (parameters.item(i).getNodeName().equals("parameter")) {
final Element parmElement = (Element) parameters.item(i);
final NamedNodeMap nm = parmElement.getAttributes();
final String parmName = nm.getNamedItem("name").getNodeValue();
final String parmValue = nm.getNamedItem("value").getNodeValue();
final Node override = nm.getNamedItem("override");
// if no override specified then default to allowed
if (override != null && !override.getNodeValue().equals("yes")) {
// can't override
} else {
// override only for adhoc or if diff from chan def
final IPortletDefinitionParameter cp = portletDef.getParameter(parmName);
if (cp == null || !cp.getValue().equals(parmValue)) {
parmStmt.clearParameters();
parmStmt.setInt(1, saveStructId);
parmStmt.setString(2, parmName);
parmStmt.setString(3, parmValue);
logger.debug(parmStmt.toString());
parmStmt.executeUpdate();
}
}
}
}
}
return saveStructId;
}
public static Document getPLF(IPerson person) throws PortalException {
try {
return (Document) person.getAttribute(Constants.PLF);
} catch (final Exception ex) {
throw new PortalException(ex);
}
}
private Element getElementForChannel(
Document doc, String chanId, IPortletDefinition def, String locale) {
final Element channel = doc.createElement("channel");
// the ID attribute is the identifier for the Channel element
channel.setAttribute("ID", chanId);
channel.setIdAttribute("ID", true);
channel.setAttribute("chanID", def.getPortletDefinitionId().getStringId());
channel.setAttribute("timeout", String.valueOf(def.getTimeout()));
if (locale != null) {
channel.setAttribute("name", def.getName(locale));
channel.setAttribute("title", def.getTitle(locale));
channel.setAttribute("description", def.getDescription(locale));
channel.setAttribute("locale", locale);
} else {
channel.setAttribute("name", def.getName());
channel.setAttribute("title", def.getTitle());
channel.setAttribute("description", def.getDescription());
}
channel.setAttribute("fname", def.getFName());
// chanClassArg is so named to highlight that we are using the argument
// to the method rather than the instance variable chanClass
channel.setAttribute("typeID", String.valueOf(def.getType().getId()));
for (final IPortletDefinitionParameter param : def.getParameters()) {
final Element parameter = doc.createElement("parameter");
parameter.setAttribute("name", param.getName());
parameter.setAttribute("value", param.getValue());
channel.appendChild(parameter);
}
return channel;
}
private interface FormOfLayoutCorruption {
boolean detect(org.dom4j.Document layout);
String getMessage();
}
private static final List<FormOfLayoutCorruption> KNOWN_FORMS_OF_LAYOUT_CORRUPTION =
Collections.unmodifiableList(
Arrays.asList(
new FormOfLayoutCorruption[] {
// One <channel> element inside another
new FormOfLayoutCorruption() {
public boolean detect(org.dom4j.Document layoutDoc) {
return !layoutDoc
.selectNodes("//channel/descendant::channel")
.isEmpty();
}
public String getMessage() {
return "one <channel> element inside another";
};
}
}));
}