/** * 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"; }; } })); }