/** * 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.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.io.Serializable; import java.util.List; import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Pattern; import net.sf.ehcache.Ehcache; import net.sf.ehcache.constructs.blocking.CacheEntryFactory; import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apereo.portal.AuthorizationException; import org.apereo.portal.IUserIdentityStore; import org.apereo.portal.IUserProfile; import org.apereo.portal.UserProfile; import org.apereo.portal.i18n.LocaleManager; import org.apereo.portal.layout.IUserLayoutStore; import org.apereo.portal.properties.PropertiesManager; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.provider.PersonImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** @since 2.5 */ @Service public class FragmentActivator { private static final String NEWLY_CREATED_ATTR = "newlyCreated"; private static final Log LOG = LogFactory.getLog(FragmentActivator.class); private final LoadingCache<String, List<Locale>> fragmentOwnerLocales = CacheBuilder.newBuilder() .<String, List<Locale>>build( new CacheLoader<String, List<Locale>>() { @Override public List<Locale> load(String key) throws Exception { return new CopyOnWriteArrayList<Locale>(); } }); private Ehcache userViews; private Ehcache userViewErrors; private IUserIdentityStore identityStore; private IUserLayoutStore userLayoutStore; private ConfigurationLoader configurationLoader; private static final String PROPERTY_ALLOW_EXPANDED_CONTENT = "org.apereo.portal.layout.dlm.allowExpandedContent"; private static final Pattern STANDARD_PATTERN = Pattern.compile("\\A[Rr][Ee][Gg][Uu][Ll][Aa][Rr]\\z"); private static final Pattern EXPANDED_PATTERN = Pattern.compile(".*"); @Autowired public void setUserViewErrors( @Qualifier("org.apereo.portal.layout.dlm.FragmentActivator.userViewErrors") Ehcache userViewErrors) { this.userViewErrors = userViewErrors; } @Autowired public void setUserViews( @Qualifier("org.apereo.portal.layout.dlm.FragmentActivator.userViews") Ehcache userViews) { this.userViews = new SelfPopulatingCache( userViews, new CacheEntryFactory() { @Override public Object createEntry(Object key) throws Exception { final UserViewKey userViewKey = (UserViewKey) key; //Check if there was an exception the last time a load attempt was made and re-throw final net.sf.ehcache.Element exceptionElement = userViewErrors.get(userViewKey); if (exceptionElement != null) { throw (Exception) exceptionElement.getObjectValue(); } try { return activateFragment(userViewKey); } catch (Exception e) { userViewErrors.put(new net.sf.ehcache.Element(userViewKey, e)); throw e; } } }); } @Autowired public void setConfigurationLoader(ConfigurationLoader configurationLoader) { this.configurationLoader = configurationLoader; } @Autowired public void setIdentityStore(IUserIdentityStore identityStore) { this.identityStore = identityStore; } @Autowired public void setUserLayoutStore(IUserLayoutStore userLayoutStore) { this.userLayoutStore = userLayoutStore; } private static class UserViewKey implements Serializable { private static final long serialVersionUID = 1L; private final String ownerId; private final Locale locale; private final int hashCode; public UserViewKey(String ownerId, Locale locale) { this.ownerId = ownerId; this.locale = locale; this.hashCode = internalHashCode(); } public String getOwnerId() { return ownerId; } public Locale getLocale() { return locale; } @Override public int hashCode() { return this.hashCode; } public int internalHashCode() { final int prime = 31; int result = 1; result = prime * result + ((ownerId == null) ? 0 : ownerId.hashCode()); result = prime * result + ((locale == null) ? 0 : locale.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; UserViewKey other = (UserViewKey) obj; if (ownerId == null) { if (other.ownerId != null) return false; } else if (!ownerId.equals(other.ownerId)) return false; if (locale == null) { if (other.locale != null) return false; } else if (!locale.equals(other.locale)) return false; return true; } @Override public String toString() { return "UserViewKey [ownerId=" + ownerId + ", locale=" + locale + "]"; } } private UserView activateFragment(final UserViewKey userViewKey) { final String ownerId = userViewKey.getOwnerId(); final FragmentDefinition fd = configurationLoader.getFragmentByOwnerId(ownerId); final Locale locale = userViewKey.getLocale(); fragmentOwnerLocales.getUnchecked(ownerId).add(locale); if (fd.isNoAudienceIncluded()) { if (LOG.isDebugEnabled()) { LOG.debug( "Skipping activation of FragmentDefinition " + fd.getName() + ", no evaluators found. " + fd); } return null; } if (LOG.isDebugEnabled()) { LOG.debug("Activating FragmentDefinition " + fd.getName() + " with locale " + locale); } IPerson owner = bindToOwner(fd); UserView view = new UserView(owner.getID()); loadLayout(view, fd, owner, locale); // if owner just created we need to push the layout into // the db so that our fragment template user is used and // not the default template user as determined by // the user identity store. if (owner.getAttribute(NEWLY_CREATED_ATTR) != null) { owner.setAttribute(Constants.PLF, view.getLayout()); try { saveLayout(view, owner); } catch (Exception e) { throw new RuntimeException( "Failed to save layout for newly created fragment owner " + owner.getUserName(), e); } } loadPreferences(view, fd); fragmentizeLayout(view, fd); if (LOG.isInfoEnabled()) { LOG.info("Activated FragmentDefinition " + fd.getName() + " with locale " + locale); } return view; } public UserView getUserView(final FragmentDefinition fd, final Locale locale) { final UserViewKey userViewKey = new UserViewKey(fd.getOwnerId(), locale); final net.sf.ehcache.Element userViewElement = this.userViews.get(userViewKey); return (UserView) userViewElement.getObjectValue(); } /** * Saves the loaded layout in the database for the user and profile. * * @param view * @param owner * @throws Exception */ private void saveLayout(UserView view, IPerson owner) throws Exception { IUserProfile profile = new UserProfile(); profile.setProfileId(view.getProfileId()); userLayoutStore.setUserLayout(owner, profile, view.getLayout(), true, false); } private IPerson bindToOwner(FragmentDefinition fragment) { IPerson owner = new PersonImpl(); owner.setAttribute("username", fragment.getOwnerId()); int userID = -1; try { userID = identityStore.getPortalUID(owner, false); } catch (AuthorizationException ae) { // current implementation of RDMBUserIdentityStore throws an // auth exception if the user doesn't exist even if // create data is false as we have it here. So this exception // can be discarded since we check for the userID being -1 // meaning that the user wasn't found to trigger creating // that user. } if (userID == -1) { userID = createOwner(owner, fragment); owner.setAttribute(NEWLY_CREATED_ATTR, "" + (userID != -1)); } owner.setID(userID); return owner; } private int createOwner(IPerson owner, FragmentDefinition fragment) { String defaultUser = null; int userID = -1; if (fragment.defaultLayoutOwnerID != null) { defaultUser = fragment.defaultLayoutOwnerID; } else { final String defaultLayoutOwner = PropertiesManager.getProperty( RDBMDistributedLayoutStore.DEFAULT_LAYOUT_OWNER_PROPERTY); if (defaultLayoutOwner != null) { defaultUser = defaultLayoutOwner; } else { try { defaultUser = PropertiesManager.getProperty( RDBMDistributedLayoutStore.TEMPLATE_USER_NAME); } catch (RuntimeException re) { throw new RuntimeException( "\n\n WARNING: defaultLayoutOwner is not specified" + " in portal.properties and no default user is " + "configured for the system. Owner '" + fragment.getOwnerId() + "' for fragment '" + fragment.getName() + "' can not be " + "created. The fragment will not be available for " + "inclusion into user layouts.\n", re); } } } if (LOG.isDebugEnabled()) { LOG.debug( "\n\nOwner '" + fragment.getOwnerId() + "' of fragment '" + fragment.getName() + "' not found. Creating as copy of '" + defaultUser + "'\n"); } if (defaultUser != null) { owner.setAttribute("uPortalTemplateUserName", defaultUser); } try { userID = identityStore.getPortalUID(owner, true); } catch (AuthorizationException ae) { throw new RuntimeException( "\n\nWARNING: Anomaly occurred while creating owner '" + fragment.getOwnerId() + "' of fragment '" + fragment.getName() + "'. The fragment will not be " + "available for inclusion into user layouts.", ae); } return userID; } private void loadLayout( UserView view, FragmentDefinition fragment, IPerson owner, Locale locale) { // if fragment not bound to user can't return any layouts. if (view.getUserId() == -1) return; // this area is hacked right now. Time won't permit how to handle // matching up multiple profiles for a fragment with an appropriate // one for incorporating into a user's layout based on their profile // when they log in with a certain user agent. The challenge is // being able to match up profiles for a user with those of a // fragment. Until this is resolved only one profile will be supported // and will have a hard coded id of 1 which is the default for profiles. // If anyone changes this user all heck could break loose for dlm. :-( Document layout = null; try { // fix hard coded 1 later for multiple profiles IUserProfile profile = userLayoutStore.getUserProfileByFname(owner, "default"); profile.setLocaleManager(new LocaleManager(owner, new Locale[] {locale})); // see if we have structure & theme stylesheets for this user yet. // If not then fall back on system's selected stylesheets. if (profile.getStructureStylesheetId() == 0 || profile.getThemeStylesheetId() == 0) profile = userLayoutStore.getSystemProfileByFname(profile.getProfileFname()); view.setProfileId(profile.getProfileId()); view.setLayoutId(profile.getLayoutId()); layout = userLayoutStore.getFragmentLayout(owner, profile); Element root = layout.getDocumentElement(); root.setAttribute( Constants.ATT_ID, Constants.FRAGMENT_ID_USER_PREFIX + view.getUserId() + Constants.FRAGMENT_ID_LAYOUT_PREFIX + view.getLayoutId()); view.setLayout(layout); } catch (Exception e) { LOG.error( "Anomaly occurred while loading layout for fragment '" + fragment.getName() + "'. The fragment will not be " + "available for inclusion into user layouts.", e); } } private void loadPreferences(UserView view, FragmentDefinition fragment) { // if fragment not bound to user can't return any preferences. if (view.getUserId() == -1) return; IPerson p = new PersonImpl(); p.setID(view.getUserId()); p.setAttribute("username", fragment.getOwnerId()); } /** * Removes unwanted and hidden folders, then changes all node ids to their globally safe * incorporated version. */ private void fragmentizeLayout(UserView view, FragmentDefinition fragment) { // if fragment not bound to user or layout empty due to error, return if (view.getUserId() == -1 || view.getLayout() == null) { return; } // Choose what types of content to apply from the fragment Pattern contentPattern = STANDARD_PATTERN; // default boolean allowExpandedContent = Boolean.parseBoolean( PropertiesManager.getProperty(PROPERTY_ALLOW_EXPANDED_CONTENT)); if (allowExpandedContent) { contentPattern = EXPANDED_PATTERN; } // remove all non-regular or hidden top level folders // skip root folder that is only child of top level layout element Element layout = view.getLayout().getDocumentElement(); Element root = (Element) layout.getFirstChild(); NodeList children = root.getChildNodes(); // process the children backwards since as we delete some the indices // shift around for (int i = children.getLength() - 1; i >= 0; i--) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("folder")) { Element folder = (Element) node; // strip out folder types 'header', 'footer' and regular, // hidden folder "User Preferences" since users have their own boolean isApplicable = contentPattern.matcher(folder.getAttribute("type")).matches(); if (!isApplicable || folder.getAttribute("hidden").equals("true")) { try { root.removeChild(folder); } catch (Exception e) { throw new RuntimeException( "Anomaly occurred while stripping out " + " portions of layout for fragment '" + fragment.getName() + "'. The fragment will not be available for " + "inclusion into user layouts.", e); } } } } // now re-lable all remaining nodes below root to have a safe system // wide id. setIdsAndAttribs( layout, layout.getAttribute(Constants.ATT_ID), "" + fragment.getIndex(), "" + fragment.getPrecedence()); } /** * Recursive method that passes through a layout tree and changes all ids from the regular * format of sXX or nXX to the globally safe incorporated id of form uXlXsXX or uXlXnXX * indicating the user id and layout id from which this node came. */ private void setIdsAndAttribs( Element parent, String labelBase, String index, String precedence) { NodeList children = parent.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { Element child = (Element) children.item(i); String id = child.getAttribute(Constants.ATT_ID); if (!id.equals("")) { String newId = labelBase + id; child.setAttribute(Constants.ATT_ID, newId); child.setIdAttribute(Constants.ATT_ID, true); child.setAttributeNS(Constants.NS_URI, Constants.ATT_FRAGMENT, index); child.setAttributeNS(Constants.NS_URI, Constants.ATT_PRECEDENCE, precedence); setIdsAndAttribs(child, labelBase, index, precedence); } } } } public void clearChacheForOwner(final String ownerId) { final List<Locale> locales = fragmentOwnerLocales.getIfPresent(ownerId); if (locales == null) { //Nothing to purge return; } for (final Locale locale : locales) { final UserViewKey userViewKey = new UserViewKey(ownerId, locale); userViews.remove(userViewKey); } } }