Get AS400 Subsystem jobs with java( Open List of Jobs (QGYOLJOB) API format OLJB0300)
(SubsystemJobOpenListTest.java, SubsystemJobListItem.java, SubsystemJobOpenList.java)
File  : SubsystemJobListItem.java
///////////////////////////////////////////////////////////////////////////////
//
//
// Filename: SubsystemJobListItem.java
//
// Author  : Vengoal Chang
// 
// Date    : 2015/07/01
//
//
///////////////////////////////////////////////////////////////////////////////
package com.vengoal.as400.list;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import com.ibm.as400.access.BinaryConverter;
public class SubsystemJobListItem {
	public static final  int ACTIVE_JOB_STATUS_FOR_JOBS_ENDING     =  103; // Active job status for jobs ending
	public static final  int CURRENT_USER                          =  305; // Current user profile
	public static final  int CPU_TIME_USED_LARGE                   =  312; // Processing unit time used - total for the job
	public static final  int CPU_TIME_USED_FOR_DATABASE            =  313; // Processing unit time used for database - total for the job (Deprecated)
	public static final  int ELAPSED_CPU_PERCENT_USED              =  314; // Processing unit used -  percent during the elapsed time (job)
	public static final  int ELAPSED_CPU_TIME_USED                 =  315; // Processing unit used - time during the elapsed time (job)
	public static final  int ELAPSED_CPU_PERCENT_USED_FOR_DATABASE =  316; // Processing unit used for database - percent during the elapsed time (job) (Deprecated)
	public static final  int ELAPSED_CPU_TIME_USED_FOR_DATABASE    =  317; // Processing unit used for database - time during the elapsed time (job) (Deprecated)
	public static final  int DATE_ENTERED_SYSTEM                   =  402; // Date and time job entered system
	public static final  int ELAPSED_DISK_IO                       =  414; // Disk I/O count during the elapsed time (job)
	public static final  int DISK_IO                               =  415; // Disk I/O count - total for the job
	public static final  int ELAPSED_DISK_IO_ASYNCH                =  416; // Disk I/O count during the elapsed time - asynchronous I/O (job)
	public static final  int ELAPSED_DISK_IO_SYNCH                 =  417; // Disk I/O count during the elapsed time - synchronous I/O (job)
	public static final  int CONTROLLED_END_REQUESTED              =  502; // End status
	public static final  int FUNCTION_NAME                         =  601; // Function name
	public static final  int FUNCTION_TYPE                         =  602; // Function type
	public static final  int INTERNAL_JOB_IDENTIFIER               =  902; // Internal job identifier
	public static final  int ELAPSED_INTERACTIVE_RESPONSE_TIME     =  904; // Interactive response time - total during the elapsed time
	public static final  int ELAPSED_INTERACTIVE_TRANSACTIONS      =  905; // Interactive transactions - count during the elapsed time
	public static final  int JOB_USER_IDENTITY                     = 1012; // Job user identity
	public static final  int JOB_END_REASON                        = 1014; // Job end reason
	public static final  int JOB_LOG_PENDING                       = 1015; // Job log pending
	public static final  int JOB_TYPE_ENHANCED                     = 1016; // Job type - enhanced
	public static final  int MEMORY_POOL                           = 1306; // Memory pool name
	public static final  int MESSAGE_REPLY                         = 1307; // Message reply
	public static final  int MESSAGE_KEY                           = 1308; // Message key, when active job waiting for a message
	public static final  int MESSAGE_QUEUE                         = 1309; // Message queue name - qualified, when active job waiting for a message
	public static final  int MESSAGE_QUEUE_ASP                     = 1310; // Message queue library ASP device name, when active job waiting for a message
	public static final  int ELAPSED_PAGE_FAULTS                   = 1609; // Page fault count during the elapsed time (job)
	public static final  int RUN_PRIORITY                          = 1802; // Run priority (job)
	public static final  int SUBSYSTEM                             = 1906; // Subsystem description name - qualified
	public static final  int SERVER_TYPE                           = 1911; // Server type
	public static final  int SPOOLED_FILE_ACTION                   = 1982; // Spooled file action
	public static final  int THREAD_COUNT                          = 2008; // Thread count
	public static final  int TEMP_STORAGE_USED_LARGE               = 2009; // Temporary storage used, in megabytes(from V7R2)
	
	private String jobName;
	private String jobUser;
	private String jobNumber;
	private String status;
	private String jobType;
	private String jobSubtype;
	private String currentUser;      // key 305
	private String functionName;     // key 601
	private String functionType;     // key 602
	private String messageReply;     // key 1307
	private byte[] messageKey;       // key 1308
	private String qualMessageQueue; // key 1309
	private String qualSubsystem;    // key 1906
	private TreeMap keyValues = new TreeMap(); // key others
	
	public SubsystemJobListItem(String jobName, String jobUser, String jobNumber,
			String status, String jobType,String jobSubtype, String currentUser, String functionName,
			String functionType,
			String messageReply, byte[] messageKey, String qualMessageQueue, String qualSubsystem) {	
		this.jobName = jobName;
		this.jobUser = jobUser;
		this.jobNumber = jobNumber;
		this.status = status;
		this.jobType = jobType;
		this.jobSubtype = jobSubtype;
		this.currentUser = currentUser;
		this.functionName = functionName;
		this.functionType = functionType;
		this.messageReply = messageReply;
		this.messageKey = messageKey;
		this.qualMessageQueue = qualMessageQueue;
		this.qualSubsystem = qualSubsystem;		
	}
	
	public Object getObject(int key){
		return keyValues.get(key);
	}
	
	public void setKeyValues(TreeMap keyValues){
		this.keyValues = keyValues;
	}
	public String getJobName() {
		return jobName;
	}
	public String getJobUser() {
		return jobUser;
	}
	public String getJobNumber() {
		return jobNumber;
	}
	public String getStatus() {
		return status;
	}
	public String getJobType() {
		return jobType;
	}
	public String getJobSubtype() {
		return jobSubtype;
	}
	public String getCurrentUser() {
		return currentUser;
	}	
	
	public String getFunctionName() {
		return functionName;
	}	
	public String getFunctionType() {
		return functionType;
	}
	public String getMessageReply() {
		return messageReply;
	}
	public byte[] getMessageKey() {
		return messageKey;
	}
	public String getQualMessageQueue() {
		return qualMessageQueue;
	}
	public String getQualSubsystem() {
		return qualSubsystem;
	}
	
	public String toString(){
		StringBuffer strBuf = new StringBuffer();
		strBuf.append(jobName).append("/");
		strBuf.append(jobUser).append("/");
		strBuf.append(jobNumber).append(",");
		strBuf.append(status).append(",");
		strBuf.append(jobType).append(",");
		strBuf.append(jobSubtype).append(",");
		strBuf.append("305=" + currentUser).append(",");
		strBuf.append("601=" + functionName).append(",");
		strBuf.append("602=" + functionType).append(",");
		strBuf.append("1307=" + messageReply).append(",");
		strBuf.append("1308(MSGKEY 4 bytes hex string)=" + BinaryConverter.bytesToHexString(messageKey)).append(",");
		strBuf.append("1309=" + qualMessageQueue).append(",");
		strBuf.append("1906=" + qualSubsystem);
		if(keyValues.size() > 0){
			 Set set = keyValues.entrySet();
			 Iterator i = set.iterator();
			 while(i.hasNext()) {				 
				 Map.Entry me = (Map.Entry)i.next();
				 strBuf.append("," + me.getKey() + "=" + me.getValue());
			}
		}
		return strBuf.toString();
	}
}
File  : SubsystemJobOpenList.java
///////////////////////////////////////////////////////////////////////////////
//
//
// Filename: SubsystemJobOpenList.java
//
// Author  : Vengoal Chang
// 
// Date    : 2015/07/01
//
//
///////////////////////////////////////////////////////////////////////////////
package com.vengoal.as400.list;
import java.io.IOException;
import java.util.TreeMap;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.AS400Exception;
import com.ibm.as400.access.AS400SecurityException;
import com.ibm.as400.access.AS400Text;
import com.ibm.as400.access.BinaryConverter;
import com.ibm.as400.access.CharConverter;
import com.ibm.as400.access.ErrorCodeParameter;
import com.ibm.as400.access.ErrorCompletingRequestException;
import com.ibm.as400.access.Job;
import com.ibm.as400.access.ObjectDoesNotExistException;
import com.ibm.as400.access.ProgramCall;
import com.ibm.as400.access.ProgramParameter;
import com.ibm.as400.access.Trace;
import com.ibm.as400.access.list.OpenList;
/**
 * Represents a list of subsystem jobs on the system with Open List of Jobs (QGYOLJOB) API. 
 * By default, following keys retrieved:
 * 			keys_[0] = 305;
 *			keys_[1] = 601;
 *			keys_[2] = 602;
 *			keys_[3] = 1307;
 *			keys_[4] = 1308;
 *			keys_[5] = 1309;
 *			keys_[6] = 1906;
 * 
 * List of Keys Supported for Format OLJB0300 reference:
 * http://www-01.ibm.com/support/knowledgecenter/ssw_ibm_i_72/apis/qgyoljob.htm?lang=en
 *
 */
public class SubsystemJobOpenList extends OpenList {
		
	private String subsystem_;
	private Job[] subsystemJobs_;
	
	// Sort keys.
	private int currentSortKey_ = 1;
		
	// Info saved between calls to load() and getJobs().
	private int numKeysReturned_;
	private int[] keyFieldsReturned_;
	private char[] keyTypesReturned_;
	private int[] keyLengthsReturned_;
	private int[] keyOffsetsReturned_;
		
	// Keys to pre-load.
	private int currentKey_ = 7;
	private int[] keys_ = new int[currentKey_];
	
	public SubsystemJobOpenList(AS400 system, String subsystem) {
		 super(system);
		 this.subsystem_ = subsystem;
			// Figure out Job information default return key fields
			keys_[0] = 305;
			keys_[1] = 601;
			keys_[2] = 602;
			keys_[3] = 1307;
			keys_[4] = 1308;
			keys_[5] = 1309;
			keys_[6] = 1906;		
	}
	
	public void addJobAttributeToRetrieve(int attribute){
		if (currentKey_ >= keys_.length){
			// Resize.
			int[] temp = keys_;
			keys_ = new int[temp.length * 2];
			System.arraycopy(temp, 0, keys_, 0, temp.length);
		}
		keys_[currentKey_++] = attribute;
	}
	
	public Job[] getSubsystemJobs(){
		return subsystemJobs_;
	}
	@Override
	protected byte[] callOpenListAPI() throws AS400SecurityException,
			ErrorCompletingRequestException, InterruptedException, IOException,
			ObjectDoesNotExistException {
		if (Trace.isTraceOn()) Trace.log(Trace.DIAGNOSTIC, "Opening spooled file list.");
		int lengthOfReceiverVariableDefinitionInformation = 4 + 20 * currentKey_;
		byte[] keyOfFieldsToBeReturned = new byte[4 * currentKey_];
		for (int i = 0; i < currentKey_; ++i)
		{
			BinaryConverter.intToByteArray(keys_[i], keyOfFieldsToBeReturned, i * 4);
		}
		
		// Figure out our sort information
		byte[] sortInformation = new byte[4 + currentSortKey_ * 12];
		BinaryConverter.intToByteArray(currentSortKey_, sortInformation, 0);
		int fieldStartingPosition = 1;
		int fieldLength = 10;
		short dataType = (short)4;
		BinaryConverter.intToByteArray(fieldStartingPosition, sortInformation, 4 );
		BinaryConverter.intToByteArray(fieldLength, sortInformation, 8);
		BinaryConverter.shortToByteArray(dataType, sortInformation, 12);
		// Sort order 0xF1 = ascending, 0xF2 = descending.
		sortInformation[14] = (byte)0xF1;
		
		// Figure out our selection criteria.
		byte[] jobSelectionInformation = new byte[206];
		
		// Generate text objects based on system CCSID.
		CharConverter conv = new CharConverter(system_.getCcsid(), system_);
		
		for (int i = 0; i < 26; ++i) jobSelectionInformation[i] = 0x40;
		String selectionJobName_ = "*ALL";
		String selectionUserName_= "*ALL";
		String selectionJobNumber_= "*ALL";
		String selectionJobType_= "*";
		conv.stringToByteArray(selectionJobName_.toUpperCase(), jobSelectionInformation, 0);
		conv.stringToByteArray(selectionUserName_.toUpperCase(), jobSelectionInformation, 10);
		conv.stringToByteArray(selectionJobNumber_, jobSelectionInformation, 20);
		conv.stringToByteArray(selectionJobType_, jobSelectionInformation, 26);
		
		int offset = 195;
		int numberOfSubsystem = 1;
		BinaryConverter.intToByteArray(offset, jobSelectionInformation, 76);
		BinaryConverter.intToByteArray(numberOfSubsystem, jobSelectionInformation, 80);
		// Subsystem name
		AS400Text subsystemText = new AS400Text(10, system_);
		byte[] subSystemBytes = subsystemText.toBytes(subsystem_);			
		System.arraycopy(subSystemBytes, 0, jobSelectionInformation, offset, 10);
		offset += 10;			 
		
		// Setup program parameters.
		ProgramParameter[] parameters = new ProgramParameter[]
		{
		    // Receiver variable, output, char(*).
		    new ProgramParameter(0),
		    // Length of receiver variable, input, binary(4).
		    new ProgramParameter(new byte[] { 0x00, 0x00, 0x00, 0x00 } ),
		    // Format name, input, char(8), EBCDIC 'OLJB0300'.
		    new ProgramParameter(new byte[] { (byte)0xD6, (byte)0xD3, (byte)0xD1, (byte)0xC2, (byte)0xF0, (byte)0xF3, (byte)0xF0, (byte)0xF0 } ),
		    // Receiver variable definition information, output, char(*).
		    new ProgramParameter(lengthOfReceiverVariableDefinitionInformation),
		    // Length of receiver variable definition information, input, binary(4).
		    new ProgramParameter(BinaryConverter.intToByteArray(lengthOfReceiverVariableDefinitionInformation)),
		    // List information, output, char(80).
		    new ProgramParameter(80),
		    // Number of records to return, input, binary(4).
		    // Special value '-1' indicates that "all records are built synchronously in the list".
		    new ProgramParameter(new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF } ),
		    // Sort information, input, char(*).
		    new ProgramParameter(sortInformation),
		    // Job selection information, input, char(*).
		    new ProgramParameter(jobSelectionInformation),
		    // Size of job selection information, input, binary(4).
		    new ProgramParameter(BinaryConverter.intToByteArray(jobSelectionInformation.length)),
		    // Number of fields to return, input, binary(4).
		    new ProgramParameter(BinaryConverter.intToByteArray(currentKey_)),
		    // Key of fields to be returned, input, array(*) of binary(4).
		    new ProgramParameter(keyOfFieldsToBeReturned),
		    // Error code, I/0, char(*).
		    new ErrorCodeParameter(),
		    // Job selection format name, input, char(8), EBCDIC 'OLJS0200'.
		    new ProgramParameter(new byte[] { (byte)0xD6, (byte)0xD3, (byte)0xD1, (byte)0xE2, (byte)0xF0, (byte)0xF2, (byte)0xF0, (byte)0xF0 } )
		};
		
		// Call the program.
		ProgramCall pc = new ProgramCall(system_, "/QSYS.LIB/QGY.LIB/QGYOLJOB.PGM", parameters);
		
		if (!pc.run())
		{
		    throw new AS400Exception(pc.getMessageList());
		}
		
		// Key information returned.
		byte[] defInfo = parameters[3].getOutputData();
		numKeysReturned_ = BinaryConverter.byteArrayToInt(defInfo, 0);
		keyFieldsReturned_ = new int[numKeysReturned_];
		keyTypesReturned_ = new char[numKeysReturned_];
		keyLengthsReturned_ = new int[numKeysReturned_];
		keyOffsetsReturned_ = new int[numKeysReturned_];
		
		offset = 4;
		for (int i = 0; i < numKeysReturned_; ++i)
		{
			keyFieldsReturned_[i] = BinaryConverter.byteArrayToInt(defInfo, offset + 4);
			keyTypesReturned_[i] = conv.byteArrayToString(defInfo, offset + 8, 1).charAt(0); // 'C' or 'B'
			keyLengthsReturned_[i] = BinaryConverter.byteArrayToInt(defInfo, offset + 12);
			keyOffsetsReturned_[i] = BinaryConverter.byteArrayToInt(defInfo, offset + 16);
			offset += 20;
		}
		
		// List information returned.
		return parameters[5].getOutputData();
	}
	@Override
	protected Object[] formatOutputData(byte[] data, int recordsReturned, int recordLength)
			throws AS400SecurityException, ErrorCompletingRequestException,
			InterruptedException, IOException, ObjectDoesNotExistException {
		int number = recordsReturned;  // request entire list
		
		CharConverter conv = new CharConverter(system_.getCcsid(), system_);
		
		SubsystemJobListItem[] listItems = new SubsystemJobListItem[number];
		subsystemJobs_ = new Job[number];
		String currentUser = null;
		String functionName = null;
		String functionType = null;
		String messageReply = null;
		byte[] messageKey = null;
		String qualMessageQueue = null;
		String qualSubsystem = null;
		TreeMap keyValues = new TreeMap();
		for (int i = 0, offset = 0; i < listItems.length; ++i, offset += recordLength)
		{
			String jobName = conv.byteArrayToString(data, offset, 10);
			String jobUser = conv.byteArrayToString(data, offset + 10, 10);
			String jobNumber = conv.byteArrayToString(data, offset + 20, 6);
			String status = conv.byteArrayToString(data, offset + 26, 4);
			String jobType = conv.byteArrayToString(data, offset + 30, 1);
			String jobSubtype = conv.byteArrayToString(data, offset + 31, 1);
			
			for (int j = 0; j < numKeysReturned_; ++j)
			{
				 int keyOffset = keyOffsetsReturned_[j];
				 if (keyTypesReturned_[j] == 'C')
				 {	
					 String value = conv.byteArrayToString(data, offset + keyOffset, keyLengthsReturned_[j]);
					 if(keyFieldsReturned_[j] == 305 ) currentUser = value;
					 if(keyFieldsReturned_[j] == 601 ) functionName = value;
					 if(keyFieldsReturned_[j] == 602 ) functionType = value;
					 if(keyFieldsReturned_[j] == 1307) messageReply = value;
					 if(keyFieldsReturned_[j] == 1309) qualMessageQueue = value;
					 if(keyFieldsReturned_[j] == 1906) qualSubsystem = value;
					 if(keyFieldsReturned_[j] == 1308)
					 {
						 byte[] msgKey = new byte[4];
						 System.arraycopy(data, offset + keyOffset, msgKey, 0, 4);
						 messageKey = msgKey;
					 }
					 if(j > 6){
						 if(keyFieldsReturned_[j] == 312 || keyFieldsReturned_[j] == 313 ||
								 keyFieldsReturned_[j] == 315 || keyFieldsReturned_[j] == 317 ||
								 keyFieldsReturned_[j] == 414 || keyFieldsReturned_[j] == 415 ||
								 keyFieldsReturned_[j] == 416 || keyFieldsReturned_[j] == 417 ||
								 keyFieldsReturned_[j] == 1609)
							 keyValues.put(keyFieldsReturned_[j], new Long(BinaryConverter.byteArrayToLong(data, offset + keyOffset))) ;
						 else
							 keyValues.put(keyFieldsReturned_[j], value) ;
					 }
				 }
				 else
				 {
					 if ((keyFieldsReturned_[j] == Job.TEMP_STORAGE_USED_LARGE))
						 keyValues.put(keyFieldsReturned_[j], new Long(BinaryConverter.byteArrayToUnsignedInt(data, offset + keyOffset))) ;
					 else
						 keyValues.put(keyFieldsReturned_[j], new Integer(BinaryConverter.byteArrayToInt(data, offset + keyOffset))) ;
				 }		
			}
			listItems[i] = new SubsystemJobListItem(jobName, jobUser, jobNumber,
						status, jobType,jobSubtype, currentUser, functionName, functionType,
						messageReply, messageKey, qualMessageQueue, qualSubsystem);
			subsystemJobs_[i] = new Job(this.getSystem(), jobName, jobUser, jobNumber);
			
			listItems[i].setKeyValues(keyValues);
		}
		return listItems;		
	}
	@Override
	protected int getBestGuessReceiverSize(int number) {		
		return 300 * number;
	}
	
}
File  : SubsystemJobOpenListTest.java
//////////////////////////////////////////////////////////////////////////////
//
//
// Filename: SubsystemJobOpenListTest.java
//
// Author  : Vengoal Chang
// 
// Date    : 2015/07/01
//
//
///////////////////////////////////////////////////////////////////////////////
package com.vengoal.as400.list;
import java.util.Enumeration;
import com.ibm.as400.access.AS400;
import com.ibm.as400.access.CallStackEntry;
import com.ibm.as400.access.Job;
import com.vengoal.as400.common.MessageUtil;
public class SubsystemJobOpenListTest {
	public static void main(String[] args) {
		AS400 as400 = new AS400("as400ip", "user", "pass");
		String subsystem = "QBATCH";
		SubsystemJobOpenList list = new SubsystemJobOpenList(as400, subsystem);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.MEMORY_POOL);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.RUN_PRIORITY);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.DATE_ENTERED_SYSTEM);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.JOB_LOG_PENDING);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.JOB_TYPE_ENHANCED);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.SPOOLED_FILE_ACTION);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.THREAD_COUNT);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.CPU_TIME_USED_LARGE);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.ELAPSED_CPU_PERCENT_USED);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.ELAPSED_CPU_TIME_USED);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.ELAPSED_PAGE_FAULTS);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.DISK_IO);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.ELAPSED_DISK_IO_ASYNCH);
		list.addJobAttributeToRetrieve(SubsystemJobListItem.ELAPSED_DISK_IO_SYNCH);
		try {
			list.open();
			Enumeration items = list.getItems();
			while (items.hasMoreElements())
			{
				SubsystemJobListItem item = (SubsystemJobListItem)items.nextElement();
				System.out.println(item);
				if(item.getMessageReply().equalsIgnoreCase(Job.MESSAGE_REPLY_WAITING)){
					System.out.println(MessageUtil.getErrMsgTxtWithAPI(as400, item.getMessageKey(), item.getQualMessageQueue()));
					Job msgwJob = new Job(as400, item.getJobName(), item.getJobUser(), item.getJobNumber());
					CallStackEntry[] callstackEntry = msgwJob.getCallStack(Job.INITIAL_THREAD);
					System.out.println("job call stack as following:");
					for(int i = 0; i< callstackEntry.length; ++i){
						//System.out.println(callstackEntry[i].getProgramLibrary() + "/" + callstackEntry[i].getProgramName() + " " + callstackEntry[i].getProcedureName());
					}
				}
				System.out.println("Spooled file action=" + item.getObject(SubsystemJobListItem.SPOOLED_FILE_ACTION));
				System.out.println("====================================");
			}
			
			Job[] subsystemJobs = list.getSubsystemJobs();            
			if(subsystemJobs != null){
				for(int i =0; i < subsystemJobs.length; ++i){
					// do your work related job 
					System.out.println(subsystemJobs[i].getNumber() + "/" + subsystemJobs[i].getUser() + "/" + subsystemJobs[i].getName());
				}
			} else {
				System.out.println("Subsystem " + subsystem + " is inactive or not exist");
			}
			list.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
						
						
參照: Open List of Jobs (QGYOLJOB) API
A blog about IBM i (AS/400), MQ and other things developers or Admins need to know.
星期四, 11月 09, 2023
2015-07-02 Get AS400 Subsystem jobs with java( Open List of Jobs (QGYOLJOB) API format OLJB0300)
訂閱:
張貼留言 (Atom)
 
沒有留言:
張貼留言