001package org.apache.turbine.services.intake;
002
003
004/*
005 * Licensed to the Apache Software Foundation (ASF) under one
006 * or more contributor license agreements.  See the NOTICE file
007 * distributed with this work for additional information
008 * regarding copyright ownership.  The ASF licenses this file
009 * to you under the Apache License, Version 2.0 (the
010 * "License"); you may not use this file except in compliance
011 * with the License.  You may obtain a copy of the License at
012 *
013 *   http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing,
016 * software distributed under the License is distributed on an
017 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
018 * KIND, either express or implied.  See the License for the
019 * specific language governing permissions and limitations
020 * under the License.
021 */
022
023
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.commons.lang.ArrayUtils;
029import org.apache.fulcrum.intake.IntakeException;
030import org.apache.fulcrum.intake.IntakeService;
031import org.apache.fulcrum.intake.Retrievable;
032import org.apache.fulcrum.intake.model.Group;
033import org.apache.fulcrum.parser.ValueParser;
034import org.apache.fulcrum.pool.Recyclable;
035import org.apache.logging.log4j.LogManager;
036import org.apache.logging.log4j.Logger;
037import org.apache.turbine.annotation.TurbineService;
038import org.apache.turbine.services.pull.ApplicationTool;
039import org.apache.turbine.util.RunData;
040
041
042/**
043 * The main class through which Intake is accessed.  Provides easy access
044 * to the Fulcrum Intake component.
045 *
046 * @author <a href="mailto:jmcnally@collab.net">John D. McNally</a>
047 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
048 * @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
049 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
050 * @version $Id$
051 */
052public class IntakeTool
053        implements ApplicationTool, Recyclable
054{
055    /** Used for logging */
056    protected static final Logger log = LogManager.getLogger(IntakeTool.class);
057
058    /** Constant for default key */
059    public static final String DEFAULT_KEY = "_0";
060
061    /** Constant for the hidden fieldname */
062    public static final String INTAKE_GRP = "intake-grp";
063
064    /** Groups from intake.xml */
065    protected HashMap<String, Group> groups = null;
066
067    /** ValueParser instance */
068    protected ValueParser pp;
069
070    private final HashMap<String, Group> declaredGroups = new HashMap<>();
071    private final StringBuilder allGroupsSB = new StringBuilder(256);
072    private final StringBuilder groupSB = new StringBuilder(128);
073
074    /** The cache of PullHelpers. **/
075    private Map<String, IntakeTool.PullHelper> pullMap = null;
076
077    /**
078     * The Intake service.
079     */
080    @TurbineService
081    protected IntakeService intakeService;
082
083    /**
084     * Constructor
085     */
086    public IntakeTool()
087    {
088    }
089
090    /**
091     * Prepares intake for a single request
092     */
093    @Override
094    public void init(Object runData)
095    {
096        if (groups == null) // Initialize only once
097        {
098            String[] groupNames = intakeService.getGroupNames();
099            int groupCount = 0;
100            if (groupNames != null)
101            {
102                groupCount = groupNames.length;
103            }
104            groups = new HashMap<>((int) (1.25 * groupCount + 1));
105            pullMap = new HashMap<>((int) (1.25 * groupCount + 1));
106
107            for (int i = groupCount - 1; i >= 0; i--)
108            {
109                pullMap.put(groupNames[i], new PullHelper(groupNames[i]));
110            }
111        }
112
113        this.pp = ((RunData) runData).getParameters();
114
115        String[] groupKeys = pp.getStrings(INTAKE_GRP);
116        String[] groupNames = null;
117        if (ArrayUtils.isEmpty(groupKeys))
118        {
119            groupNames = intakeService.getGroupNames();
120        }
121        else
122        {
123            groupNames = new String[groupKeys.length];
124            for (int i = groupKeys.length - 1; i >= 0; i--)
125            {
126                groupNames[i] = intakeService.getGroupName(groupKeys[i]);
127            }
128        }
129
130        for (int i = groupNames.length - 1; i >= 0; i--)
131        {
132            try
133            {
134                List<Group> foundGroups = intakeService.getGroup(groupNames[i])
135                    .getObjects(pp);
136
137                if (foundGroups != null)
138                {
139                    foundGroups.forEach(
140                            group -> groups.put(group.getObjectKey(), group));
141                }
142            }
143            catch (IntakeException e)
144            {
145                log.error(e);
146            }
147        }
148    }
149
150    /**
151     * Add all registered group ids to the value parser
152     *
153     * @param vp the value parser
154     */
155    public void addGroupsToParameters(ValueParser vp)
156    {
157        for (Group group : groups.values())
158        {
159            if (!declaredGroups.containsKey(group.getIntakeGroupName()))
160            {
161                declaredGroups.put(group.getIntakeGroupName(), null);
162                vp.add("intake-grp", group.getGID());
163            }
164            vp.add(group.getGID(), group.getOID());
165        }
166        declaredGroups.clear();
167    }
168
169    /**
170     * A convenience method to write out the hidden form fields
171     * that notify intake of the relevant groups.  It should be used
172     * only in templates with 1 form.  In multiform templates, the groups
173     * that are relevant for each form need to be declared using
174     * $intake.newForm() and $intake.declareGroup($group) for the relevant
175     * groups in the form.
176     *
177     * @return the HTML that declares all groups to Intake in hidden input fields
178     *
179     */
180    public String declareGroups()
181    {
182        allGroupsSB.setLength(0);
183        for (Group group : groups.values())
184        {
185            declareGroup(group, allGroupsSB);
186        }
187        return allGroupsSB.toString();
188    }
189
190    /**
191     * A convenience method to write out the hidden form fields
192     * that notify intake of the group.
193     *
194     * @param group the group to declare
195     * @return the HTML that declares the group to Intake in a hidden input field
196     */
197    public String declareGroup(Group group)
198    {
199        groupSB.setLength(0);
200        declareGroup(group, groupSB);
201        return groupSB.toString();
202    }
203
204    /**
205     * xhtml valid hidden input field(s) that notifies intake of the
206     * group's presence.
207     * @param group the group to declare
208     * @param sb a String Builder where the hidden field HTML will be appended
209     */
210    public void declareGroup(Group group, StringBuilder sb)
211    {
212        if (!declaredGroups.containsKey(group.getIntakeGroupName()))
213        {
214            declaredGroups.put(group.getIntakeGroupName(), null);
215            sb.append("<input type=\"hidden\" name=\"")
216                    .append(INTAKE_GRP)
217                    .append("\" value=\"")
218                    .append(group.getGID())
219                    .append("\"/>\n");
220        }
221        group.appendHtmlFormInput(sb);
222    }
223
224    /**
225     * Declare that a new form starts
226     */
227    public void newForm()
228    {
229        declaredGroups.clear();
230        for (Group group : groups.values())
231        {
232            group.resetDeclared();
233        }
234    }
235
236    /**
237     * Implementation of ApplicationTool interface is not needed for this
238     * tool as it is request scoped
239     */
240    @Override
241    public void refresh()
242    {
243        // empty
244    }
245
246    /**
247     * Inner class to present a nice interface to the template designer
248     */
249    public class PullHelper
250    {
251        /** Name of the group used by the pull helper */
252        String groupName;
253
254        /**
255         * Protected constructor to force use of factory method.
256         *
257         * @param groupName the group name
258         */
259        protected PullHelper(String groupName)
260        {
261            this.groupName = groupName;
262        }
263
264        /**
265         * Populates the object with the default values from the XML File
266         *
267         * @return a Group object with the default values
268         * @throws IntakeException if getting the group fails
269         */
270        public Group getDefault()
271                throws IntakeException
272        {
273            return setKey(DEFAULT_KEY);
274        }
275
276        /**
277         * Calls setKey(key,true)
278         *
279         * @param key the group key
280         * @return an Intake Group
281         * @throws IntakeException if getting the group fails
282         */
283        public Group setKey(String key)
284                throws IntakeException
285        {
286            return setKey(key, true);
287        }
288
289        /**
290         * Return the group identified by its key
291         *
292         * @param key the group key
293         * @param create true if a non-existing group should be created
294         * @return an Intake Group
295         * @throws IntakeException if getting the group fails
296         */
297        public Group setKey(String key, boolean create)
298                throws IntakeException
299        {
300            Group g = null;
301
302            String inputKey = intakeService.getGroupKey(groupName) + key;
303            if (groups.containsKey(inputKey))
304            {
305                g = groups.get(inputKey);
306            }
307            else if (create)
308            {
309                g = intakeService.getGroup(groupName);
310                groups.put(inputKey, g);
311                g.init(key, pp);
312            }
313
314            return g;
315        }
316
317        /**
318         * maps an Intake Group to the values from a Retrievable object.
319         *
320         * @param obj A retrievable object
321         * @return an Intake Group
322         */
323        public Group mapTo(Retrievable obj)
324        {
325            Group g = null;
326
327            try
328            {
329                String inputKey = intakeService.getGroupKey(groupName)
330                        + obj.getQueryKey();
331                if (groups.containsKey(inputKey))
332                {
333                    g = groups.get(inputKey);
334                }
335                else
336                {
337                    g = intakeService.getGroup(groupName);
338                    groups.put(inputKey, g);
339                }
340
341                return g.init(obj);
342            }
343            catch (IntakeException e)
344            {
345                log.error(e);
346            }
347
348            return null;
349        }
350    }
351
352    /**
353     * get a specific group
354     * @param groupName the name of the group
355     * @return a {@link PullHelper} wrapper around the group
356     */
357    public PullHelper get(String groupName)
358    {
359        return pullMap.get(groupName);
360    }
361
362    /**
363     * Get a specific group
364     *
365     * @param groupName the name of the group
366     * @param throwExceptions if false, exceptions will be suppressed.
367     * @return a {@link PullHelper} wrapper around the group
368     * @throws IntakeException could not retrieve group
369     */
370    public PullHelper get(String groupName, boolean throwExceptions)
371            throws IntakeException
372    {
373        return pullMap.get(groupName);
374    }
375
376    /**
377     * Loops through all of the Groups and checks to see if
378     * the data within the Group is valid.
379     * @return true if all groups are valid
380     */
381    public boolean isAllValid()
382    {
383        boolean allValid = true;
384        for (Group group : groups.values())
385        {
386            allValid &= group.isAllValid();
387        }
388        return allValid;
389    }
390
391    /**
392     * Get a specific group by name and key.
393     * @param groupName the name of the group
394     * @param key the key for the group
395     * @return the {@link Group}
396     * @throws IntakeException if the group could not be retrieved
397     */
398    public Group get(String groupName, String key)
399            throws IntakeException
400    {
401        return get(groupName, key, true);
402    }
403
404    /**
405     * Get a specific group by name and key. Also specify
406     * whether or not you want to create a new group.
407     * @param groupName the name of the group
408     * @param key the key for the group
409     * @param create true if a new group should be created
410     * @return the {@link Group}
411     * @throws IntakeException if the group could not be retrieved
412     */
413    public Group get(String groupName, String key, boolean create)
414            throws IntakeException
415    {
416        if (groupName == null)
417        {
418            throw new IntakeException("intakeService.get: groupName == null");
419        }
420        if (key == null)
421        {
422            throw new IntakeException("intakeService.get: key == null");
423        }
424
425        PullHelper ph = get(groupName);
426        return (ph == null) ? null : ph.setKey(key, create);
427    }
428
429    /**
430     * Removes group.  Primary use is to remove a group that has
431     * been processed by an action and is no longer appropriate
432     * in the view (screen).
433     * @param group the group instance to remove
434     */
435    public void remove(Group group)
436    {
437        if (group != null)
438        {
439            groups.remove(group.getObjectKey());
440            group.removeFromRequest();
441
442            String[] groupKeys = pp.getStrings(INTAKE_GRP);
443
444            pp.remove(INTAKE_GRP);
445
446                        if (groupKeys != null)
447                        {
448                        for (String groupKey : groupKeys)
449                {
450                            if (!groupKey.equals(group.getGID()))
451                            {
452                                 pp.add(INTAKE_GRP, groupKey);
453                            }
454                }
455                    }
456
457            try
458            {
459                intakeService.releaseGroup(group);
460            }
461            catch (IntakeException ie)
462            {
463                log.error("Tried to release unknown group {}", group.getIntakeGroupName(), ie);
464            }
465        }
466    }
467
468    /**
469     * Removes all groups.  Primary use is to remove groups that have
470     * been processed by an action and are no longer appropriate
471     * in the view (screen).
472     */
473    public void removeAll()
474    {
475        Object[] allGroups = groups.values().toArray();
476        for (int i = allGroups.length - 1; i >= 0; i--)
477        {
478            Group group = (Group) allGroups[i];
479            remove(group);
480        }
481    }
482
483    /**
484     * Get a Map containing all the groups.
485     *
486     * @return the Group Map
487     */
488    public Map<String, Group> getGroups()
489    {
490        return groups;
491    }
492
493    // ****************** Recyclable implementation ************************
494
495    private boolean disposed;
496
497    /**
498     * Recycles the object for a new client. Recycle methods with
499     * parameters must be added to implementing object and they will be
500     * automatically called by pool implementations when the object is
501     * taken from the pool for a new client. The parameters must
502     * correspond to the parameters of the constructors of the object.
503     * For new objects, constructors can call their corresponding recycle
504     * methods whenever applicable.
505     * The recycle methods must call their super.
506     */
507    @Override
508    public void recycle()
509    {
510        disposed = false;
511    }
512
513    /**
514     * Disposes the object after use. The method is called
515     * when the object is returned to its pool.
516     * The dispose method must call its super.
517     */
518    @Override
519    public void dispose()
520    {
521        for (Group group : groups.values())
522        {
523            try
524            {
525                intakeService.releaseGroup(group);
526            }
527            catch (IntakeException ie)
528            {
529                log.error("Tried to release unknown group {}",
530                        group.getIntakeGroupName(), ie);
531            }
532        }
533
534        groups.clear();
535        declaredGroups.clear();
536        pp = null;
537
538        disposed = true;
539    }
540
541    /**
542     * Checks whether the recyclable has been disposed.
543     *
544     * @return true, if the recyclable is disposed.
545     */
546    @Override
547    public boolean isDisposed()
548    {
549        return disposed;
550    }
551}